├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── LICENSE ├── README.adoc ├── build.gradle.kts ├── gitlfs-client ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── client │ │ ├── AuthHelper.kt │ │ ├── BatchDownloader.kt │ │ ├── BatchSettings.kt │ │ ├── BatchUploader.kt │ │ ├── Client.kt │ │ ├── HttpExecutor.kt │ │ ├── auth │ │ ├── AuthProvider.kt │ │ ├── BasicAuthProvider.kt │ │ ├── CachedAuthProvider.kt │ │ └── ExternalAuthProvider.kt │ │ ├── exceptions │ │ ├── ForbiddenException.kt │ │ ├── RequestException.kt │ │ └── UnauthorizedException.kt │ │ ├── internal │ │ ├── BatchWorker.kt │ │ ├── HttpClientExecutor.kt │ │ ├── JsonPost.kt │ │ ├── LfsRequest.kt │ │ ├── LockCreate.kt │ │ ├── LockDelete.kt │ │ ├── LocksList.kt │ │ ├── MetaGet.kt │ │ ├── MetaPost.kt │ │ ├── ObjectGet.kt │ │ ├── ObjectPut.kt │ │ ├── ObjectVerify.kt │ │ ├── Request.kt │ │ └── Work.kt │ │ └── io │ │ ├── ByteArrayStreamProvider.kt │ │ ├── FileStreamProvider.kt │ │ ├── StreamHandler.kt │ │ ├── StreamProvider.kt │ │ ├── StringStreamProvider.kt │ │ └── UrlStreamProvider.kt │ └── test │ ├── kotlin │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── client │ │ ├── AuthHelperTest.kt │ │ ├── ClientBatchTest.kt │ │ ├── ClientLegacyTest.kt │ │ ├── ClientLocksTest.kt │ │ ├── Examples.kt │ │ ├── FakeAuthProvider.kt │ │ ├── HttpRecord.kt │ │ ├── HttpRecorder.kt │ │ ├── HttpReplay.kt │ │ ├── Recorder.kt │ │ ├── YamlConstructor.kt │ │ ├── YamlHelper.kt │ │ └── YamlRepresenter.kt │ └── resources │ └── ru │ └── bozaro │ └── gitlfs │ └── client │ ├── batch-download-01.yml │ ├── batch-download-02.yml │ ├── batch-upload-01.yml │ ├── batch-upload-02.yml │ ├── batch-upload-chunked.yml │ ├── legacy-download-01.yml │ ├── legacy-download-02.yml │ ├── legacy-upload-01.yml │ ├── legacy-upload-02.yml │ ├── legacy-upload-03.yml │ ├── legacy-upload-04.yml │ └── locking-01.yml ├── gitlfs-common ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── common │ │ ├── Constants.kt │ │ ├── JsonHelper.kt │ │ ├── LockConflictException.kt │ │ ├── VerifyLocksResult.kt │ │ ├── data │ │ ├── BatchItem.kt │ │ ├── BatchReq.kt │ │ ├── BatchRes.kt │ │ ├── CreateLockReq.kt │ │ ├── CreateLockRes.kt │ │ ├── DeleteLockReq.kt │ │ ├── DeleteLockRes.kt │ │ ├── Error.kt │ │ ├── Link.kt │ │ ├── LinkType.kt │ │ ├── Links.kt │ │ ├── Lock.kt │ │ ├── LockConflictRes.kt │ │ ├── LocksRes.kt │ │ ├── Meta.kt │ │ ├── ObjectRes.kt │ │ ├── Operation.kt │ │ ├── Ref.kt │ │ ├── User.kt │ │ ├── VerifyLocksReq.kt │ │ └── VerifyLocksRes.kt │ │ └── io │ │ └── InputStreamValidator.kt │ └── test │ ├── kotlin │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── common │ │ └── data │ │ ├── BatchReqTest.kt │ │ ├── BatchResTest.kt │ │ ├── CreateLockReqTest.kt │ │ ├── CreateLockResTest.kt │ │ ├── DateTest.kt │ │ ├── DeleteLockReqTest.kt │ │ ├── LinkTest.kt │ │ ├── LocksResTest.kt │ │ ├── MetaTest.kt │ │ ├── ObjectResTest.kt │ │ ├── SerializeTester.kt │ │ ├── VerifyLocksReqTest.kt │ │ └── VerifyLocksResTest.kt │ └── resources │ └── ru │ └── bozaro │ └── gitlfs │ └── common │ └── data │ ├── batch-req-01.json │ ├── batch-res-01.json │ ├── create-lock-req-01.json │ ├── create-lock-res-01.json │ ├── delete-lock-req-01.json │ ├── link-01.json │ ├── link-02.json │ ├── locks-res-01.json │ ├── meta-01.json │ ├── object-res-01.json │ ├── object-res-02.json │ ├── verify-locks-req-01.json │ └── verify-locks-res-01.json ├── gitlfs-pointer ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── pointer │ │ ├── Constants.kt │ │ └── Pointer.kt │ └── test │ ├── kotlin │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── pointer │ │ └── PointerTest.kt │ └── resources │ └── ru │ └── bozaro │ └── gitlfs │ └── pointer │ ├── pointer-invalid-01.dat │ ├── pointer-invalid-02.dat │ ├── pointer-invalid-03.dat │ ├── pointer-invalid-04.dat │ ├── pointer-invalid-05.dat │ ├── pointer-invalid-06.dat │ ├── pointer-invalid-07.dat │ ├── pointer-invalid-08.dat │ ├── pointer-invalid-09.dat │ ├── pointer-invalid-10.dat │ ├── pointer-valid-01.dat │ ├── pointer-valid-02.dat │ └── pointer-valid-03.dat ├── gitlfs-server ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── ru │ │ └── bozaro │ │ └── gitlfs │ │ └── server │ │ ├── ContentManager.kt │ │ ├── ContentServlet.kt │ │ ├── ForbiddenError.kt │ │ ├── LocalPointerManager.kt │ │ ├── LockManager.kt │ │ ├── LocksServlet.kt │ │ ├── PointerManager.kt │ │ ├── PointerServlet.kt │ │ ├── ServerError.kt │ │ ├── UnauthorizedError.kt │ │ └── internal │ │ ├── ObjectResponse.kt │ │ └── ResponseWriter.kt │ └── test │ └── kotlin │ └── ru │ └── bozaro │ └── gitlfs │ └── server │ ├── BatchTest.kt │ ├── EmbeddedHttpServer.kt │ ├── EmbeddedLfsServer.kt │ ├── LocksTest.kt │ ├── MemoryLockManager.kt │ ├── MemoryStorage.kt │ └── ServerTest.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dat -text 2 | *.yml text eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gradle" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test 6 | if: "!startsWith(github.ref, 'refs/tags/')" 7 | strategy: 8 | matrix: 9 | include: 10 | - java: 11 11 | - java: 17 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Set up Java 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'adopt' 20 | java-version: ${{ matrix.java }} 21 | - name: Run tests 22 | uses: eskatos/gradle-command-action@v3 23 | with: 24 | arguments: test 25 | publish: 26 | runs-on: ubuntu-latest 27 | needs: [ test ] 28 | # See https://github.com/actions/runner/issues/491#issuecomment-850884422 for explanation on why always() is needed 29 | if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') && github.repository == 'git-as-svn/git-lfs-java' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Java 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: 'adopt' 37 | java-version: 11 # Build releases using oldest supported jdk 38 | - name: Publish to Sonatype 39 | uses: eskatos/gradle-command-action@v3 40 | env: 41 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 42 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 43 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 44 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 45 | with: 46 | arguments: publish closeAndReleaseStagingRepositories 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.local 3 | 4 | # Package Files # 5 | *.jar 6 | *.war 7 | *.ear 8 | 9 | # Idea 10 | *.iml 11 | .idea 12 | 13 | # Gradle 14 | .gradle 15 | .maven 16 | build 17 | 18 | # Virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 19 | hs_err_pid* -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 83 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-java 2 | :project-handle: git-lfs-java 3 | :slug: bozaro/{project-handle} 4 | :uri-project: https://github.com/{slug} 5 | :uri-ci: {uri-project}/actions?query=branch%3Amaster 6 | 7 | image:{uri-project}/actions/workflows/ci.yml/badge.svg?branch=master[Build Status,link={uri-ci}] 8 | image:https://img.shields.io/maven-central/v/ru.bozaro.gitlfs/gitlfs-common.svg[Maven Central,link=http://mvnrepository.com/artifact/ru.bozaro.gitlfs] 9 | 10 | == What is this? 11 | 12 | This is Git LFS Java API implementation. 13 | 14 | This project contains: 15 | 16 | * gitlfs-common - Common structures for serialization/deserialization Git LFS messages 17 | * gitlfs-pointer - Git LFS pointer serialization/deserialization 18 | * gitlfs-client - API for uploading/downloading Git LFS objects from server 19 | * gitlfs-server - Servlets for creating custom LFS server 20 | 21 | == How to use? 22 | 23 | You can download the latest stable from http://mvnrepository.com/artifact/ru.bozaro.gitlfs[Maven Central]. 24 | 25 | === Downloading object from Git LFS server 26 | 27 | [source,java] 28 | ---- 29 | include::gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt[tags=download] 30 | ---- 31 | 32 | === Uploading object to Git LFS server 33 | 34 | [source,java] 35 | ---- 36 | include::gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt[tags=upload] 37 | ---- 38 | 39 | === Embedded LFS server 40 | 41 | See link:gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/ServerTest.kt[] for example. 42 | 43 | == Changelog 44 | 45 | == 0.19.0 46 | 47 | * Downgrade SLF4J to 1.7.x 48 | 49 | === 0.18.0 50 | 51 | * Drop Java 8 support 52 | * Upgrade to Jakarta Servlet 5.0 53 | * Upgrade to Jetty 11 54 | 55 | === 0.17.0 56 | 57 | * Properly handle `Transfer-Encoding: chunked`. 58 | See https://github.com/bozaro/git-as-svn/issues/365[bozaro/git-as-svn#365] 59 | 60 | === 0.16.0 61 | 62 | * Update dependencies 63 | 64 | === 0.15.2 65 | 66 | * Send error message when lock create fails due to already existing lock 67 | 68 | === 0.15.1 69 | 70 | * Fix compatibility with GitLab LFS locks API 71 | 72 | === 0.15.0 73 | 74 | * Introduce Client.openObject methods 75 | 76 | === 0.14.1 77 | 78 | * Fix object verification to be compatible with git-lfs client 79 | 80 | === 0.14.0 81 | 82 | * Add LFS object verification server code 83 | * Fix broken handling of already existing LFS object 84 | 85 | === 0.13.3 86 | 87 | * Update dependencies 88 | * Fix deprecated Jackson API usage 89 | 90 | === 0.13.2 91 | 92 | * Fix ISO 8601 date formatting again 93 | 94 | === 0.13.1 95 | 96 | * LFS locking API fixes 97 | * Drop dependency on Guava 98 | 99 | === 0.13.0 100 | 101 | * https://github.com/git-lfs/git-lfs/blob/master/docs/api/locking.md[LFS locking] support 102 | 103 | === 0.12.1 104 | 105 | * Fix compatibility with Gitea LFS 106 | 107 | === 0.12.0 108 | 109 | * Update dependencies 110 | * Do not output \r in JSON on Windows 111 | 112 | === 0.11.1 113 | 114 | * Fix ISO 8601 date formatting 115 | 116 | === 0.11.0 117 | 118 | * JFrog Artifactory git-lfs compatibility (see #4) 119 | 120 | === 0.10.0 121 | 122 | * Add request/response content information to RequestException 123 | 124 | === 0.9.0 125 | 126 | * Update all dependencies 127 | 128 | === 0.8.0 129 | 130 | * Replace commons-httpclient:commons-httpclient:3.1 by org.apache.httpcomponents:httpclient:4.1.3 131 | 132 | === 0.7.0 133 | 134 | * Add header modification for replacing Basic authentication by Token 135 | * Don't ask password for SSH authentication 136 | * Add more informative HTTP error message 137 | 138 | === 0.6.0 139 | 140 | * Require JDK 8 141 | * High level batch API implementation 142 | * Add steam hash validation on download 143 | 144 | === 0.5.0 145 | 146 | * Server implemetation stabilization 147 | * Create multitheaded HttpClient by default 148 | * Fix single object downloading API 149 | 150 | === 0.4.0 151 | 152 | * Initial server implementation 153 | * Fix url concatenation 154 | * Fix downloading for not uploaded object (404 error) 155 | * Fix minor bugs 156 | 157 | === 0.3.0 158 | 159 | * Add authenticator for git-lfs-authenticate command (experimental) 160 | * Add AuthHelper class for simple AuthProvider creation 161 | * Fix verify url bug 162 | * Fix basic authentication 163 | * Fix already uploaded behaviour 164 | 165 | === 0.2.0 166 | 167 | * Support https://github.com/github/git-lfs/blob/master/docs/api/http-v1-batch.md[Git LFS v1 Batch API] 168 | 169 | === 0.1.0 170 | 171 | * Initial version; 172 | * Support https://github.com/github/git-lfs/blob/master/docs/api/http-v1-original.md[Git LFS v1 Original API] 173 | * Support https://github.com/github/git-lfs/blob/master/docs/spec.md[Git LFS pointer serialize/parse] 174 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.ajoberstar.grgit.Grgit 2 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 3 | import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | 6 | val ossrhUsername: String? = System.getenv("OSSRH_USERNAME") 7 | val ossrhPassword: String? = System.getenv("OSSRH_PASSWORD") 8 | val signingKey: String? = System.getenv("SIGNING_KEY") 9 | val signingPassword: String? = System.getenv("SIGNING_PASSWORD") 10 | 11 | tasks.wrapper { 12 | gradleVersion = "8.5" 13 | distributionType = Wrapper.DistributionType.ALL 14 | } 15 | 16 | plugins { 17 | id("com.github.ben-manes.versions") version "0.52.0" 18 | id("io.github.gradle-nexus.publish-plugin") version "2.0.0" 19 | id("org.ajoberstar.grgit") version "5.3.0" 20 | kotlin("jvm") version "2.1.21" apply false 21 | idea 22 | } 23 | 24 | val javaVersion = JavaVersion.VERSION_11 25 | 26 | allprojects { 27 | group = "ru.bozaro.gitlfs" 28 | version = "0.21.0-SNAPSHOT" 29 | 30 | apply(plugin = "idea") 31 | apply(plugin = "com.github.ben-manes.versions") 32 | apply(plugin = "org.jetbrains.kotlin.jvm") 33 | 34 | tasks.withType { 35 | kotlinOptions.jvmTarget = javaVersion.toString() 36 | } 37 | 38 | repositories { 39 | mavenCentral() 40 | } 41 | } 42 | 43 | idea { 44 | project.jdkName = javaVersion.name 45 | project.languageLevel = IdeaLanguageLevel(javaVersion) 46 | } 47 | 48 | subprojects { 49 | apply(plugin = "java-library") 50 | apply(plugin = "maven-publish") 51 | apply(plugin = "signing") 52 | 53 | configure { 54 | sourceCompatibility = javaVersion 55 | targetCompatibility = javaVersion 56 | } 57 | 58 | val api by configurations 59 | val testImplementation by configurations 60 | 61 | dependencies { 62 | api("com.google.code.findbugs:jsr305:3.0.2") 63 | 64 | testImplementation("com.google.guava:guava:33.4.8-jre") 65 | testImplementation("org.testng:testng:7.11.0") 66 | } 67 | 68 | idea { 69 | module { 70 | isDownloadJavadoc = true 71 | isDownloadSources = true 72 | 73 | // Workaround for https://youtrack.jetbrains.com/issue/IDEA-175172 74 | outputDir = file("build/classes/main") 75 | testOutputDir = file("build/classes/test") 76 | } 77 | } 78 | 79 | tasks.withType { 80 | options.encoding = "UTF-8" 81 | } 82 | 83 | tasks.withType { 84 | useTestNG { 85 | testLogging { 86 | exceptionFormat = TestExceptionFormat.FULL 87 | showStandardStreams = true 88 | } 89 | } 90 | } 91 | 92 | val javadoc by tasks.getting(Javadoc::class) { 93 | (options as? CoreJavadocOptions)?.addStringOption("Xdoclint:none", "-quiet") 94 | } 95 | 96 | val javadocJar by tasks.registering(Jar::class) { 97 | from(javadoc) 98 | archiveClassifier.set("javadoc") 99 | } 100 | 101 | val sourcesJar by tasks.registering(Jar::class) { 102 | val sourceSets: SourceSetContainer by project 103 | from(sourceSets["main"].allSource) 104 | archiveClassifier.set("sources") 105 | } 106 | 107 | configure { 108 | publications { 109 | create(project.name) { 110 | from(components["java"]) 111 | 112 | artifact(sourcesJar.get()) 113 | artifact(javadocJar.get()) 114 | 115 | pom { 116 | name.set(project.name) 117 | 118 | val pomDescription = description 119 | afterEvaluate { 120 | pomDescription.set(project.description) 121 | } 122 | 123 | url.set("https://github.com/bozaro/git-lfs-java") 124 | 125 | scm { 126 | connection.set("scm:git:git://github.com/bozaro/git-lfs-java.git") 127 | tag.set(Grgit.open(mapOf("dir" to rootDir)).head().id) 128 | url.set("https://github.com/bozaro/git-lfs-java") 129 | } 130 | 131 | licenses { 132 | license { 133 | name.set("Lesser General Public License, version 3 or greater") 134 | url.set("https://www.gnu.org/licenses/lgpl-3.0.html") 135 | } 136 | } 137 | 138 | developers { 139 | developer { 140 | id.set("bozaro") 141 | name.set("Artem V. Navrotskiy") 142 | email.set("bozaro@yandex.ru") 143 | } 144 | 145 | developer { 146 | id.set("slonopotamus") 147 | name.set("Marat Radchenko") 148 | email.set("marat@slonopotamus.org") 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | configure { 157 | isRequired = signingKey != "" 158 | 159 | useInMemoryPgpKeys(signingKey, signingPassword) 160 | 161 | val publishing: PublishingExtension by project.extensions 162 | sign(publishing.publications) 163 | } 164 | } 165 | 166 | nexusPublishing { 167 | repositories { 168 | sonatype { 169 | packageGroup.set("ru.bozaro") 170 | stagingProfileId.set("365bc6dc8b7aa3") 171 | username.set(ossrhUsername) 172 | password.set(ossrhPassword) 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /gitlfs-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Java Git-LFS client library" 2 | 3 | dependencies { 4 | api(project(":gitlfs-common")) 5 | api("org.apache.httpcomponents:httpclient:4.5.14") 6 | implementation("org.slf4j:slf4j-api:1.7.36") 7 | 8 | testImplementation("org.yaml:snakeyaml:1.33") 9 | testRuntimeOnly("org.slf4j:slf4j-simple:1.7.36") 10 | } 11 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/AuthHelper.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import ru.bozaro.gitlfs.client.auth.AuthProvider 4 | import ru.bozaro.gitlfs.client.auth.BasicAuthProvider 5 | import ru.bozaro.gitlfs.client.auth.ExternalAuthProvider 6 | import java.net.MalformedURLException 7 | import java.net.URI 8 | import java.net.URISyntaxException 9 | 10 | /** 11 | * Utility class. 12 | * 13 | * @author Artem V. Navrotskiy 14 | */ 15 | object AuthHelper { 16 | /** 17 | * Create AuthProvider by gitURL. 18 | * 19 | * 20 | * Supported URL formats: 21 | * 22 | * 23 | * * https://user:passw0rd@github.com/foo/bar.git 24 | * * http://user:passw0rd@github.com/foo/bar.git 25 | * * git://user:passw0rd@github.com/foo/bar.git 26 | * * ssh://git@github.com/foo/bar.git 27 | * * git@github.com:foo/bar.git 28 | * 29 | * 30 | * Detail Git URL format: https://git-scm.com/book/ch4-1.html 31 | * 32 | * @param gitURL URL to repository. 33 | * @return Created auth provider. 34 | */ 35 | @kotlin.jvm.JvmStatic 36 | @Throws(MalformedURLException::class) 37 | fun create(gitURL: String): AuthProvider { 38 | if (gitURL.contains("://")) { 39 | val uri = URI.create(gitURL) 40 | val path = uri.path 41 | return when (uri.scheme) { 42 | "https", "http", "git" -> BasicAuthProvider(join(uri, "info/lfs")) 43 | "ssh" -> ExternalAuthProvider(uri.authority, if (path.startsWith("/")) path.substring(1) else path) 44 | else -> throw MalformedURLException("Can't find authenticator for scheme: " + uri.scheme) 45 | } 46 | } 47 | return ExternalAuthProvider(gitURL) 48 | } 49 | 50 | fun join(href: URI, vararg path: String): URI { 51 | return try { 52 | var uri = URI(href.scheme, href.authority, href.path + if (href.path.endsWith("/")) "" else "/", null, null) 53 | for (fragment in path) uri = uri.resolve(fragment) 54 | uri 55 | } catch (e: URISyntaxException) { 56 | throw IllegalStateException(e) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchDownloader.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import ru.bozaro.gitlfs.client.internal.BatchWorker 4 | import ru.bozaro.gitlfs.client.internal.Work 5 | import ru.bozaro.gitlfs.client.io.StreamHandler 6 | import ru.bozaro.gitlfs.common.data.BatchItem 7 | import ru.bozaro.gitlfs.common.data.LinkType 8 | import ru.bozaro.gitlfs.common.data.Meta 9 | import ru.bozaro.gitlfs.common.data.Operation 10 | import java.io.IOException 11 | import java.util.concurrent.CompletableFuture 12 | import java.util.concurrent.ExecutorService 13 | 14 | /** 15 | * Batching downloader client. 16 | * 17 | * @author Artem V. Navrotskiy 18 | */ 19 | class BatchDownloader constructor(client: Client, pool: ExecutorService, settings: BatchSettings = BatchSettings()) : 20 | BatchWorker, Any?>(client, pool, settings, Operation.Download) { 21 | fun download(meta: Meta, callback: StreamHandler): CompletableFuture { 22 | return enqueue(meta, callback) as CompletableFuture 23 | } 24 | 25 | override fun objectTask(state: State, Any?>, item: BatchItem): Work? { 26 | // Invalid links data 27 | if (!item.links.containsKey(LinkType.Download)) { 28 | state.future.completeExceptionally(IOException("Download link not found")) 29 | return null 30 | } 31 | // Already processed 32 | return Work { client.getObject(item, item, state.context) } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchSettings.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import ru.bozaro.gitlfs.common.Constants.BATCH_SIZE 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | 7 | /** 8 | * Batch settings. 9 | * 10 | * @author Artem V. Navrotskiy @users.noreply.github.com> 11 | */ 12 | class BatchSettings { 13 | /** 14 | * Maximum objects in batch request. 15 | */ 16 | var limit: Int = BATCH_SIZE 17 | private set 18 | 19 | /** 20 | * Minimum download/upload requests in queue for next batch request. 21 | */ 22 | var threshold = 10 23 | private set 24 | 25 | /** 26 | * Retry on failure count. 27 | */ 28 | var retryCount = 3 29 | private set 30 | 31 | constructor() 32 | constructor(limit: Int, threshold: Int, retryCount: Int) { 33 | this.limit = limit 34 | this.threshold = threshold 35 | this.retryCount = retryCount 36 | } 37 | 38 | fun setLimit(limit: Int): BatchSettings { 39 | this.limit = min(limit, 1) 40 | return this 41 | } 42 | 43 | fun setThreshold(threshold: Int): BatchSettings { 44 | this.threshold = max(threshold, 0) 45 | return this 46 | } 47 | 48 | fun setRetryCount(retryCount: Int): BatchSettings { 49 | this.retryCount = max(retryCount, 1) 50 | return this 51 | } 52 | 53 | override fun toString(): String { 54 | return "BatchSettings{" + 55 | "limit=" + limit + 56 | ", threshold=" + threshold + 57 | ", retryCount=" + retryCount + 58 | '}' 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/BatchUploader.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import ru.bozaro.gitlfs.client.internal.BatchWorker 4 | import ru.bozaro.gitlfs.client.internal.Work 5 | import ru.bozaro.gitlfs.client.io.StreamProvider 6 | import ru.bozaro.gitlfs.common.data.BatchItem 7 | import ru.bozaro.gitlfs.common.data.LinkType 8 | import ru.bozaro.gitlfs.common.data.Meta 9 | import ru.bozaro.gitlfs.common.data.Operation 10 | import java.util.concurrent.CompletableFuture 11 | import java.util.concurrent.ExecutorService 12 | 13 | /** 14 | * Batching uploader client. 15 | * 16 | * @author Artem V. Navrotskiy 17 | */ 18 | class BatchUploader constructor(client: Client, pool: ExecutorService, settings: BatchSettings = BatchSettings()) : 19 | BatchWorker(client, pool, settings, Operation.Upload) { 20 | /** 21 | * This method computes stream metadata and upload object. 22 | * 23 | * @param streamProvider Stream provider. 24 | * @return Return future with upload result. 25 | */ 26 | fun upload(streamProvider: StreamProvider): CompletableFuture { 27 | val future = CompletableFuture() 28 | pool.submit { 29 | try { 30 | future.complete(Client.generateMeta(streamProvider)) 31 | } catch (e: Throwable) { 32 | future.completeExceptionally(e) 33 | } 34 | } 35 | return future.thenCompose { meta: Meta -> upload(meta, streamProvider) } 36 | } 37 | 38 | /** 39 | * This method start uploading object to server. 40 | * 41 | * @param meta Object metadata. 42 | * @param streamProvider Stream provider. 43 | * @return Return future with upload result. For same objects can return same future. 44 | */ 45 | fun upload(meta: Meta, streamProvider: StreamProvider): CompletableFuture { 46 | return enqueue(meta, streamProvider) 47 | } 48 | 49 | override fun objectTask(state: State, item: BatchItem): Work? { 50 | return if (item.links.containsKey(LinkType.Upload)) { 51 | // Wait for upload. 52 | Work { 53 | client.putObject(state.context, state.meta, item) 54 | null 55 | } 56 | } else { 57 | // Already uploaded. 58 | state.future.complete(state.meta) 59 | null 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/HttpExecutor.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.apache.http.client.methods.CloseableHttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | import java.io.Closeable 6 | import java.io.IOException 7 | 8 | /** 9 | * Abstract class for HTTP connection execution. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | interface HttpExecutor : Closeable { 14 | @Throws(IOException::class) 15 | fun executeMethod(request: HttpUriRequest): CloseableHttpResponse 16 | } 17 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/AuthProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.auth 2 | 3 | import ru.bozaro.gitlfs.common.data.Link 4 | import ru.bozaro.gitlfs.common.data.Operation 5 | import java.io.IOException 6 | 7 | /** 8 | * Authentication provider. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | interface AuthProvider { 13 | /** 14 | * Get auth ru.bozaro.gitlfs.common.data. 15 | * Auth ru.bozaro.gitlfs.common.data can be cached in this method. 16 | * 17 | * @param operation Operation type. 18 | * @return ru.bozaro.gitlfs.common.data. 19 | */ 20 | @Throws(IOException::class) 21 | fun getAuth(operation: Operation): Link 22 | 23 | /** 24 | * Set auth as expired. 25 | * 26 | * @param operation Operation type. 27 | * @param auth Expired auth ru.bozaro.gitlfs.common.data. 28 | */ 29 | fun invalidateAuth(operation: Operation, auth: Link) 30 | } 31 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/BasicAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.auth 2 | 3 | import org.apache.commons.codec.binary.Base64 4 | import ru.bozaro.gitlfs.common.Constants.HEADER_AUTHORIZATION 5 | import ru.bozaro.gitlfs.common.data.Link 6 | import ru.bozaro.gitlfs.common.data.Operation 7 | import java.net.URI 8 | import java.net.URISyntaxException 9 | import java.nio.charset.StandardCharsets 10 | import java.util.* 11 | 12 | /** 13 | * Auth provider for basic authentication. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class BasicAuthProvider constructor(href: URI, login: String? = null, password: String? = null) : AuthProvider { 18 | private var auth: Link 19 | 20 | override fun getAuth(operation: Operation): Link { 21 | return auth 22 | } 23 | 24 | override fun invalidateAuth(operation: Operation, auth: Link) {} 25 | 26 | companion object { 27 | private fun isEmpty(value: String?): Boolean { 28 | return value == null || value.isEmpty() 29 | } 30 | 31 | private fun extractLogin(userInfo: String?): String { 32 | if (userInfo == null) return "" 33 | val separator = userInfo.indexOf(':') 34 | return if (separator >= 0) userInfo.substring(0, separator) else userInfo 35 | } 36 | 37 | private fun extractPassword(userInfo: String?): String { 38 | if (userInfo == null) return "" 39 | val separator = userInfo.indexOf(':') 40 | return if (separator >= 0) userInfo.substring(separator + 1) else "" 41 | } 42 | } 43 | 44 | init { 45 | val authLogin: String? = if (isEmpty(login)) { 46 | extractLogin(href.userInfo) 47 | } else { 48 | login 49 | } 50 | val authPassword: String? = if (isEmpty(password)) { 51 | extractPassword(href.userInfo) 52 | } else { 53 | password 54 | } 55 | val header = TreeMap() 56 | val userInfo = "$authLogin:$authPassword" 57 | header[HEADER_AUTHORIZATION] = 58 | "Basic " + String(Base64.encodeBase64(userInfo.toByteArray(StandardCharsets.UTF_8)), StandardCharsets.UTF_8) 59 | try { 60 | val scheme = if ("git" == href.scheme) "https" else href.scheme 61 | auth = Link(URI(scheme, href.authority, href.path, null, null), Collections.unmodifiableMap(header), null) 62 | } catch (e: URISyntaxException) { 63 | throw IllegalStateException(e) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/CachedAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.auth 2 | 3 | import ru.bozaro.gitlfs.common.data.Link 4 | import ru.bozaro.gitlfs.common.data.Operation 5 | import java.io.IOException 6 | import java.util.* 7 | import java.util.concurrent.ConcurrentHashMap 8 | import java.util.concurrent.ConcurrentMap 9 | 10 | /** 11 | * Get authentication ru.bozaro.gitlfs.common.data from external application. 12 | * This AuthProvider is EXPERIMENTAL and it can only be used at your own risk. 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | abstract class CachedAuthProvider : AuthProvider { 17 | private val authCache: ConcurrentMap 18 | 19 | private val locks: EnumMap = createLocks() 20 | 21 | @Throws(IOException::class) 22 | override fun getAuth(operation: Operation): Link { 23 | var auth = authCache[operation] 24 | if (auth == null) { 25 | synchronized(locks[operation]!!) { 26 | auth = authCache[operation] 27 | if (auth == null) { 28 | try { 29 | auth = getAuthUncached(operation) 30 | authCache[operation] = auth 31 | } catch (e: InterruptedException) { 32 | throw IOException(e) 33 | } 34 | } 35 | } 36 | } 37 | return auth!! 38 | } 39 | 40 | @Throws(IOException::class, InterruptedException::class) 41 | protected abstract fun getAuthUncached(operation: Operation): Link 42 | 43 | override fun invalidateAuth(operation: Operation, auth: Link) { 44 | authCache.remove(operation, auth) 45 | } 46 | 47 | companion object { 48 | private fun createLocks(): EnumMap { 49 | val result = EnumMap(Operation::class.java) 50 | for (value in Operation.values()) { 51 | result[value] = Any() 52 | } 53 | return result 54 | } 55 | } 56 | 57 | init { 58 | authCache = ConcurrentHashMap(Operation.values().size) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/auth/ExternalAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.auth 2 | 3 | import ru.bozaro.gitlfs.common.JsonHelper 4 | import ru.bozaro.gitlfs.common.data.Link 5 | import ru.bozaro.gitlfs.common.data.Operation 6 | import java.io.ByteArrayOutputStream 7 | import java.io.IOException 8 | import java.net.MalformedURLException 9 | 10 | /** 11 | * Get authentication ru.bozaro.gitlfs.common.data from external application. 12 | * This AuthProvider is EXPERIMENTAL and it can only be used at your own risk. 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | open class ExternalAuthProvider : CachedAuthProvider { 17 | private val authority: String 18 | private val path: String 19 | 20 | /** 21 | * Create authentication wrapper for git-lfs-authenticate command. 22 | * 23 | * @param gitUrl Git URL like: git@github.com:bozaro/git-lfs-java.git 24 | */ 25 | constructor(gitUrl: String) { 26 | val separator = gitUrl.indexOf(':') 27 | if (separator < 0) { 28 | throw MalformedURLException("Can't find separator ':' in gitUrl: $gitUrl") 29 | } 30 | authority = gitUrl.substring(0, separator) 31 | path = gitUrl.substring(separator + 1) 32 | } 33 | 34 | /** 35 | * Create authentication wrapper for git-lfs-authenticate command. 36 | * 37 | * @param authority SSH server authority with user name 38 | * @param path Repostiry path 39 | */ 40 | constructor(authority: String, path: String) { 41 | this.authority = authority 42 | this.path = path 43 | } 44 | 45 | @Throws(IOException::class, InterruptedException::class) 46 | override fun getAuthUncached(operation: Operation): Link { 47 | val builder = ProcessBuilder() 48 | .command(*getCommand(operation)) 49 | .redirectError(ProcessBuilder.Redirect.PIPE) 50 | .redirectOutput(ProcessBuilder.Redirect.PIPE) 51 | val process = builder.start() 52 | process.outputStream.close() 53 | val stdoutStream = process.inputStream 54 | val stdoutData = ByteArrayOutputStream() 55 | val buffer = ByteArray(0x10000) 56 | while (true) { 57 | val read = stdoutStream.read(buffer) 58 | if (read <= 0) break 59 | stdoutData.write(buffer, 0, read) 60 | } 61 | val exitValue = process.waitFor() 62 | if (exitValue != 0) { 63 | throw IOException("Command returned with non-zero exit code " + exitValue + ": " + builder.command().toTypedArray().contentToString()) 64 | } 65 | return JsonHelper.mapper.readValue(stdoutData.toByteArray(), Link::class.java) 66 | } 67 | 68 | private fun getCommand(operation: Operation): Array { 69 | return arrayOf( 70 | "ssh", 71 | authority, 72 | "-oBatchMode=yes", 73 | "-C", 74 | "git-lfs-authenticate", 75 | path, 76 | operation.toValue() 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/ForbiddenException.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.exceptions 2 | 3 | import org.apache.http.HttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | 6 | /** 7 | * Forbidden HTTP exception. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | class ForbiddenException(request: HttpUriRequest, response: HttpResponse) : RequestException(request, response) 12 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/RequestException.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.exceptions 2 | 3 | import org.apache.http.HttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | import java.io.IOException 6 | 7 | /** 8 | * Simple HTTP exception. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | open class RequestException(private val request: HttpUriRequest, private val response: HttpResponse) : IOException() { 13 | val statusCode: Int 14 | get() = response.statusLine.statusCode 15 | override val message: String 16 | get() { 17 | val statusLine = response.statusLine 18 | return request.uri.toString() + " - " + statusLine.statusCode + " (" + statusLine.reasonPhrase + ")" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/exceptions/UnauthorizedException.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.exceptions 2 | 3 | import org.apache.http.HttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | 6 | /** 7 | * Unauthorized HTTP exception. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | class UnauthorizedException(request: HttpUriRequest, response: HttpResponse) : RequestException(request, response) 12 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/HttpClientExecutor.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import org.apache.http.client.methods.CloseableHttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | import org.apache.http.impl.client.CloseableHttpClient 6 | import ru.bozaro.gitlfs.client.HttpExecutor 7 | import java.io.IOException 8 | 9 | /** 10 | * Simple HttpClient wrapper. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class HttpClientExecutor(private val http: CloseableHttpClient) : HttpExecutor { 15 | @Throws(IOException::class) 16 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse { 17 | return http.execute(request) 18 | } 19 | 20 | @Throws(IOException::class) 21 | override fun close() { 22 | http.close() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/JsonPost.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.apache.http.HttpResponse 6 | import org.apache.http.client.methods.HttpPost 7 | import org.apache.http.entity.ByteArrayEntity 8 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 9 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 10 | import java.io.IOException 11 | 12 | /** 13 | * POST simple JSON request. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class JsonPost(private val req: Req, private val type: Class) : Request { 18 | @Throws(JsonProcessingException::class) 19 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 20 | val method = HttpPost(url) 21 | method.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 22 | val entity = ByteArrayEntity(mapper.writeValueAsBytes(req)) 23 | entity.setContentType(MIME_LFS_JSON) 24 | method.entity = entity 25 | return LfsRequest(method, entity) 26 | } 27 | 28 | @Throws(IOException::class) 29 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Res { 30 | return mapper.readValue(response.entity.content, type) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LfsRequest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import org.apache.http.client.methods.HttpUriRequest 4 | import org.apache.http.entity.AbstractHttpEntity 5 | import org.apache.http.protocol.HTTP 6 | 7 | class LfsRequest internal constructor(private val request: HttpUriRequest, private val entity: AbstractHttpEntity?) { 8 | fun addHeaders(headers: Map): HttpUriRequest { 9 | for ((key, value) in headers) { 10 | if (HTTP.TRANSFER_ENCODING == key) { 11 | /* 12 | See https://github.com/bozaro/git-as-svn/issues/365 13 | LFS-server can ask us to respond with chunked body via setting "Transfer-Encoding: chunked" HTTP header in LFS link 14 | Unfortunately, we cannot pass it as-is to response HTTP headers, see RequestContent#process. 15 | If it sees that Transfer-Encoding header was set, it throws exception immediately. 16 | So instead, we suppress addition of Transfer-Encoding header and set entity to be chunked here. 17 | RequestContent#process will see that HttpEntity#isChunked returns true and will set correct Transfer-Encoding header. 18 | */ 19 | if (entity != null) { 20 | val chunked = HTTP.CHUNK_CODING == value 21 | entity.isChunked = chunked 22 | } 23 | } else { 24 | request.addHeader(key, value) 25 | } 26 | } 27 | return request 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LockCreate.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.apache.http.HttpResponse 6 | import org.apache.http.HttpStatus 7 | import org.apache.http.client.methods.HttpPost 8 | import org.apache.http.entity.AbstractHttpEntity 9 | import org.apache.http.entity.ByteArrayEntity 10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 12 | import ru.bozaro.gitlfs.common.data.* 13 | import java.io.IOException 14 | 15 | class LockCreate(private val path: String, private val ref: Ref?) : Request { 16 | @Throws(JsonProcessingException::class) 17 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 18 | val req = HttpPost(url) 19 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 20 | val createLockReq = CreateLockReq(path, ref) 21 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(createLockReq)) 22 | entity.setContentType(MIME_LFS_JSON) 23 | req.entity = entity 24 | return LfsRequest(req, entity) 25 | } 26 | 27 | @Throws(IOException::class) 28 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Res { 29 | return when (response.statusLine.statusCode) { 30 | HttpStatus.SC_CREATED -> Res(true, mapper.readValue(response.entity.content, CreateLockRes::class.java).lock, null) 31 | HttpStatus.SC_CONFLICT -> { 32 | val res = mapper.readValue(response.entity.content, LockConflictRes::class.java) 33 | Res(false, res.lock, res.message) 34 | } 35 | else -> throw IllegalStateException() 36 | } 37 | } 38 | 39 | override fun statusCodes(): IntArray { 40 | return intArrayOf( 41 | HttpStatus.SC_CREATED, 42 | HttpStatus.SC_CONFLICT) 43 | } 44 | 45 | class Res(val isSuccess: Boolean, val lock: Lock, val message: String?) 46 | } 47 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LockDelete.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.apache.http.HttpResponse 6 | import org.apache.http.HttpStatus 7 | import org.apache.http.client.methods.HttpPost 8 | import org.apache.http.entity.AbstractHttpEntity 9 | import org.apache.http.entity.ByteArrayEntity 10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 12 | import ru.bozaro.gitlfs.common.data.DeleteLockReq 13 | import ru.bozaro.gitlfs.common.data.DeleteLockRes 14 | import ru.bozaro.gitlfs.common.data.Lock 15 | import ru.bozaro.gitlfs.common.data.Ref 16 | import java.io.IOException 17 | 18 | class LockDelete(private val force: Boolean, private val ref: Ref?) : Request { 19 | @Throws(JsonProcessingException::class) 20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 21 | val req = HttpPost(url) 22 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 23 | val createLockReq = DeleteLockReq(force, ref) 24 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(createLockReq)) 25 | entity.setContentType(MIME_LFS_JSON) 26 | req.entity = entity 27 | return LfsRequest(req, entity) 28 | } 29 | 30 | @Throws(IOException::class) 31 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Lock? { 32 | return when (response.statusLine.statusCode) { 33 | HttpStatus.SC_OK -> mapper.readValue(response.entity.content, DeleteLockRes::class.java).lock 34 | HttpStatus.SC_NOT_FOUND -> null 35 | else -> throw IllegalStateException() 36 | } 37 | } 38 | 39 | override fun statusCodes(): IntArray { 40 | return intArrayOf( 41 | HttpStatus.SC_OK, 42 | HttpStatus.SC_NOT_FOUND 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/LocksList.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.client.methods.HttpGet 6 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 7 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 8 | import ru.bozaro.gitlfs.common.data.LocksRes 9 | import java.io.IOException 10 | 11 | class LocksList : Request { 12 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 13 | val req = HttpGet(url) 14 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 15 | return LfsRequest(req, null) 16 | } 17 | 18 | @Throws(IOException::class) 19 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): LocksRes { 20 | return mapper.readValue(response.entity.content, LocksRes::class.java) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/MetaGet.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.HttpStatus 6 | import org.apache.http.client.methods.HttpGet 7 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 8 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 9 | import ru.bozaro.gitlfs.common.data.ObjectRes 10 | import java.io.IOException 11 | 12 | /** 13 | * GET object metadata request. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class MetaGet : Request { 18 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 19 | val req = HttpGet(url) 20 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 21 | return LfsRequest(req, null) 22 | } 23 | 24 | @Throws(IOException::class) 25 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): ObjectRes? { 26 | return when (response.statusLine.statusCode) { 27 | HttpStatus.SC_OK -> mapper.readValue(response.entity.content, ObjectRes::class.java) 28 | HttpStatus.SC_NOT_FOUND -> null 29 | else -> throw IllegalStateException() 30 | } 31 | } 32 | 33 | override fun statusCodes(): IntArray { 34 | return intArrayOf( 35 | HttpStatus.SC_OK, 36 | HttpStatus.SC_NOT_FOUND 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/MetaPost.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.apache.http.HttpResponse 6 | import org.apache.http.HttpStatus 7 | import org.apache.http.client.methods.HttpPost 8 | import org.apache.http.entity.AbstractHttpEntity 9 | import org.apache.http.entity.ByteArrayEntity 10 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 11 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 12 | import ru.bozaro.gitlfs.common.data.Meta 13 | import ru.bozaro.gitlfs.common.data.ObjectRes 14 | import java.io.IOException 15 | 16 | /** 17 | * POST object metadata request. 18 | * 19 | * @author Artem V. Navrotskiy 20 | */ 21 | class MetaPost(private val meta: Meta) : Request { 22 | @Throws(JsonProcessingException::class) 23 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 24 | val req = HttpPost(url) 25 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 26 | val entity: AbstractHttpEntity = ByteArrayEntity(mapper.writeValueAsBytes(meta)) 27 | entity.setContentType(MIME_LFS_JSON) 28 | req.entity = entity 29 | return LfsRequest(req, entity) 30 | } 31 | 32 | @Throws(IOException::class) 33 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): ObjectRes? { 34 | return when (response.statusLine.statusCode) { 35 | HttpStatus.SC_OK -> null 36 | HttpStatus.SC_ACCEPTED -> mapper.readValue(response.entity.content, ObjectRes::class.java) 37 | else -> throw IllegalStateException() 38 | } 39 | } 40 | 41 | override fun statusCodes(): IntArray { 42 | return intArrayOf( 43 | HttpStatus.SC_OK, 44 | HttpStatus.SC_ACCEPTED 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectGet.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.client.methods.HttpGet 6 | import ru.bozaro.gitlfs.client.io.StreamHandler 7 | import java.io.IOException 8 | 9 | /** 10 | * GET object request. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class ObjectGet(private val handler: StreamHandler) : Request { 15 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 16 | val req = HttpGet(url) 17 | return LfsRequest(req, null) 18 | } 19 | 20 | @Throws(IOException::class) 21 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): T { 22 | return handler.accept(response.entity.content) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectPut.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.HttpStatus 6 | import org.apache.http.client.methods.HttpPut 7 | import org.apache.http.entity.AbstractHttpEntity 8 | import org.apache.http.entity.InputStreamEntity 9 | import ru.bozaro.gitlfs.client.io.StreamProvider 10 | import ru.bozaro.gitlfs.common.Constants.MIME_BINARY 11 | import java.io.IOException 12 | 13 | /** 14 | * PUT object request. 15 | * 16 | * @author Artem V. Navrotskiy 17 | */ 18 | class ObjectPut(private val streamProvider: StreamProvider, private val size: Long) : Request { 19 | @Throws(IOException::class) 20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 21 | val req = HttpPut(url) 22 | val entity: AbstractHttpEntity = InputStreamEntity(streamProvider.stream, size) 23 | entity.setContentType(MIME_BINARY) 24 | req.entity = entity 25 | return LfsRequest(req, entity) 26 | } 27 | 28 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Void? { 29 | return null 30 | } 31 | 32 | override fun statusCodes(): IntArray { 33 | return intArrayOf( 34 | HttpStatus.SC_OK, 35 | HttpStatus.SC_CREATED 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/ObjectVerify.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.client.methods.HttpPost 6 | import org.apache.http.entity.AbstractHttpEntity 7 | import org.apache.http.entity.ByteArrayEntity 8 | import ru.bozaro.gitlfs.common.Constants.HEADER_ACCEPT 9 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 10 | import ru.bozaro.gitlfs.common.data.Meta 11 | import java.io.IOException 12 | 13 | /** 14 | * Object verification after upload request. 15 | * 16 | * @author Artem V. Navrotskiy 17 | */ 18 | class ObjectVerify(private val meta: Meta) : Request { 19 | @Throws(IOException::class) 20 | override fun createRequest(mapper: ObjectMapper, url: String): LfsRequest { 21 | val req = HttpPost(url) 22 | req.addHeader(HEADER_ACCEPT, MIME_LFS_JSON) 23 | val content = mapper.writeValueAsBytes(meta) 24 | val entity: AbstractHttpEntity = ByteArrayEntity(content) 25 | entity.setContentType(MIME_LFS_JSON) 26 | req.entity = entity 27 | return LfsRequest(req, entity) 28 | } 29 | 30 | override fun processResponse(mapper: ObjectMapper, response: HttpResponse): Void? { 31 | return null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/Request.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.apache.http.HttpResponse 5 | import org.apache.http.HttpStatus 6 | import java.io.IOException 7 | 8 | /** 9 | * Single HTTP request. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | interface Request { 14 | @Throws(IOException::class) 15 | fun createRequest(mapper: ObjectMapper, url: String): LfsRequest 16 | 17 | @Throws(IOException::class) 18 | fun processResponse(mapper: ObjectMapper, response: HttpResponse): R 19 | 20 | /** 21 | * Success status codes. 22 | * 23 | * @return Success status codes. 24 | */ 25 | fun statusCodes(): IntArray { 26 | return intArrayOf(HttpStatus.SC_OK) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/internal/Work.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.internal 2 | 3 | import ru.bozaro.gitlfs.common.data.Link 4 | import java.io.IOException 5 | 6 | /** 7 | * Work with same auth ru.bozaro.gitlfs.common.data. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | fun interface Work { 12 | @Throws(IOException::class) 13 | fun exec(auth: Link): R 14 | } 15 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/ByteArrayStreamProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.InputStream 5 | 6 | /** 7 | * Create stream by bytes. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | open class ByteArrayStreamProvider(private val data: ByteArray) : StreamProvider { 12 | override val stream: InputStream 13 | get() = ByteArrayInputStream(data) 14 | } 15 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/FileStreamProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.io.File 4 | import java.io.FileInputStream 5 | import java.io.IOException 6 | import java.io.InputStream 7 | 8 | /** 9 | * Create stream from file. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | class FileStreamProvider(private val file: File) : StreamProvider { 14 | @get:Throws(IOException::class) 15 | override val stream: InputStream 16 | get() = FileInputStream(file) 17 | } 18 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StreamHandler.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | 6 | /** 7 | * Interface for handle stream of downloading ru.bozaro.gitlfs.common.data. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | fun interface StreamHandler { 12 | @Throws(IOException::class) 13 | fun accept(inputStream: InputStream): T 14 | } 15 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StreamProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | 6 | /** 7 | * Stream provider. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | interface StreamProvider { 12 | @get:Throws(IOException::class) 13 | val stream: InputStream 14 | } 15 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/StringStreamProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.nio.charset.StandardCharsets 4 | 5 | /** 6 | * Create stream from string. 7 | * 8 | * @author Artem V. Navrotskiy 9 | */ 10 | class StringStreamProvider(data: String) : ByteArrayStreamProvider(data.toByteArray(StandardCharsets.UTF_8)) 11 | -------------------------------------------------------------------------------- /gitlfs-client/src/main/kotlin/ru/bozaro/gitlfs/client/io/UrlStreamProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client.io 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | import java.net.URL 6 | 7 | /** 8 | * Create stream by URL. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | class UrlStreamProvider(private val url: URL) : StreamProvider { 13 | @get:Throws(IOException::class) 14 | override val stream: InputStream 15 | get() = url.openStream() 16 | } 17 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/AuthHelperTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.DataProvider 5 | import org.testng.annotations.Test 6 | import ru.bozaro.gitlfs.client.AuthHelper.join 7 | import java.net.URI 8 | 9 | /** 10 | * Tests for AuthHelper. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class AuthHelperTest { 15 | @Test(dataProvider = "joinUrlProvider") 16 | fun joinUrl(base: String, str: String, expected: String) { 17 | Assert.assertEquals(join(URI.create(base), str), URI.create(expected)) 18 | } 19 | 20 | @DataProvider(name = "joinUrlProvider") 21 | fun joinUrlProvider(): Array> { 22 | return arrayOf( 23 | arrayOf("http://test.ru/foo", "bar", "http://test.ru/foo/bar"), 24 | arrayOf("http://test.ru/foo/", "bar", "http://test.ru/foo/bar"), 25 | arrayOf("http://test.ru/foo", "/bar", "http://test.ru/bar"), 26 | arrayOf("http://test.ru/foo/", "/bar", "http://test.ru/bar"), 27 | arrayOf("https://test.ru/foo/", "http://foo.ru/bar", "http://foo.ru/bar") 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/ClientLegacyTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import com.google.common.io.ByteStreams 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import ru.bozaro.gitlfs.client.exceptions.ForbiddenException 7 | import ru.bozaro.gitlfs.client.io.StringStreamProvider 8 | import java.io.FileNotFoundException 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.nio.charset.StandardCharsets 12 | 13 | /** 14 | * Replay tests for https://github.com/github/git-lfs/blob/master/docs/api/http-v1-original.md 15 | * 16 | * @author Artem V. Navrotskiy 17 | */ 18 | class ClientLegacyTest { 19 | /** 20 | * Simple upload. 21 | */ 22 | @Test 23 | @Throws(IOException::class) 24 | fun legacyUpload01() { 25 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-01.yml") 26 | val client = Client(FakeAuthProvider(false), replay) 27 | Assert.assertTrue(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015"))) 28 | replay.close() 29 | } 30 | 31 | /** 32 | * Forbidden. 33 | */ 34 | @Test(expectedExceptions = [ForbiddenException::class]) 35 | @Throws(IOException::class) 36 | fun legacyUpload02() { 37 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-02.yml") 38 | val client = Client(FakeAuthProvider(false), replay) 39 | Assert.assertFalse(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015"))) 40 | replay.close() 41 | } 42 | 43 | /** 44 | * Expired token. 45 | */ 46 | @Test 47 | @Throws(IOException::class) 48 | fun legacyUpload03() { 49 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-03.yml") 50 | val client = Client(FakeAuthProvider(false), replay) 51 | Assert.assertTrue(client.putObject(StringStreamProvider("Fri Oct 02 21:07:33 MSK 2015"))) 52 | replay.close() 53 | } 54 | 55 | /** 56 | * Already uploaded, 57 | */ 58 | @Test 59 | @Throws(IOException::class) 60 | fun legacyUpload04() { 61 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-upload-04.yml") 62 | val client = Client(FakeAuthProvider(false), replay) 63 | Assert.assertFalse(client.putObject(StringStreamProvider("Hello, world!!!"))) 64 | replay.close() 65 | } 66 | 67 | /** 68 | * Simple download 69 | */ 70 | @Test 71 | @Throws(IOException::class) 72 | fun legacyDownload01() { 73 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-download-01.yml") 74 | val client = Client(FakeAuthProvider(false), replay) 75 | val data = 76 | client.getObject("b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e") { `in`: InputStream -> 77 | ByteStreams.toByteArray(`in`) 78 | } 79 | Assert.assertEquals(String(data, StandardCharsets.UTF_8), "Fri Oct 02 21:07:33 MSK 2015") 80 | replay.close() 81 | } 82 | 83 | /** 84 | * Download not uploaded object 85 | */ 86 | @Test 87 | @Throws(IOException::class) 88 | fun legacyDownload02() { 89 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/legacy-download-02.yml") 90 | val client = Client(FakeAuthProvider(false), replay) 91 | try { 92 | client.getObject("01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b") { `in`: InputStream -> 93 | ByteStreams.toByteArray(`in`) 94 | } 95 | Assert.fail() 96 | } catch (ignored: FileNotFoundException) { 97 | } 98 | replay.close() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/ClientLocksTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import ru.bozaro.gitlfs.common.LockConflictException 6 | import ru.bozaro.gitlfs.common.data.Ref 7 | import java.io.IOException 8 | 9 | class ClientLocksTest { 10 | @Test 11 | @Throws(IOException::class, LockConflictException::class) 12 | fun simple() { 13 | val ref = Ref.create("refs/heads/master") 14 | val replay = YamlHelper.createReplay("/ru/bozaro/gitlfs/client/locking-01.yml") 15 | val client = Client(FakeAuthProvider(false), replay) 16 | val lock = client.lock("build.gradle", ref) 17 | Assert.assertNotNull(lock) 18 | Assert.assertNotNull(lock.id) 19 | Assert.assertEquals(lock.path, "build.gradle") 20 | try { 21 | client.lock("build.gradle", ref) 22 | Assert.fail() 23 | } catch (e: LockConflictException) { 24 | Assert.assertEquals(e.lock.id, lock.id) 25 | } 26 | run { 27 | val locks = client.listLocks("build.gradle", null, ref) 28 | Assert.assertEquals(locks.size, 1) 29 | Assert.assertEquals(locks[0].id, lock.id) 30 | } 31 | run { 32 | val locks = client.verifyLocks(ref) 33 | Assert.assertEquals(locks.ourLocks.size, 2) 34 | Assert.assertEquals(locks.ourLocks[1].id, lock.id) 35 | Assert.assertEquals(locks.theirLocks.size, 0) 36 | } 37 | val unlock = client.unlock(lock.id, true, ref) 38 | Assert.assertNotNull(unlock) 39 | Assert.assertEquals(unlock!!.id, lock.id) 40 | Assert.assertNull(client.unlock(lock.id, false, ref)) 41 | replay.close() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Examples.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import com.google.common.io.ByteStreams 4 | import ru.bozaro.gitlfs.client.AuthHelper.create 5 | import ru.bozaro.gitlfs.client.io.FileStreamProvider 6 | import ru.bozaro.gitlfs.common.data.Meta 7 | import java.io.File 8 | import java.io.IOException 9 | import java.io.InputStream 10 | import java.util.concurrent.Executors 11 | 12 | /** 13 | * This class provides examples for documentation. 14 | */ 15 | class Examples { 16 | @Throws(IOException::class) 17 | fun download() { 18 | // tag::download[] 19 | val auth = create("git@github.com:foo/bar.git") 20 | val client = Client(auth) 21 | 22 | // Single object 23 | val content = client.getObject("4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") { `in`: InputStream -> ByteStreams.toByteArray(`in`) } 24 | 25 | // Batch mode 26 | val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 27 | val downloader = BatchDownloader(client, pool) 28 | val future = downloader.download(Meta("4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393", 10)) { `in`: InputStream -> ByteStreams.toByteArray(`in`) } 29 | // end::download[] 30 | } 31 | 32 | @Throws(Exception::class) 33 | fun upload() { 34 | // tag::upload[] 35 | val auth = create("git@github.com:foo/bar.git") 36 | val client = Client(auth) 37 | 38 | // Single object 39 | client.putObject(FileStreamProvider(File("foo.bin"))) 40 | 41 | // Batch mode 42 | val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 43 | val uploader = BatchUploader(client, pool) 44 | val future = uploader.upload(FileStreamProvider(File("bar.bin"))) 45 | // end::upload[] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/FakeAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import ru.bozaro.gitlfs.client.auth.AuthProvider 5 | import ru.bozaro.gitlfs.common.data.Link 6 | import ru.bozaro.gitlfs.common.data.Operation 7 | import java.net.URI 8 | import java.util.concurrent.atomic.AtomicInteger 9 | 10 | /** 11 | * Fake authenticator. 12 | * 13 | * @author Artem V. Navrotskiy 14 | */ 15 | class FakeAuthProvider(private val chunkedUpload: Boolean) : AuthProvider { 16 | private val lock = Any() 17 | 18 | private val id = AtomicInteger(0) 19 | private var auth: Array? = null 20 | 21 | override fun getAuth(operation: Operation): Link { 22 | synchronized(lock) { 23 | if (auth == null) { 24 | auth = createAuth() 25 | } 26 | return auth!![operation.ordinal] 27 | } 28 | } 29 | 30 | override fun invalidateAuth(operation: Operation, auth: Link) { 31 | synchronized(lock) { 32 | if (this.auth != null && (this.auth!![0] == auth || this.auth!![1] == auth)) { 33 | this.auth = null 34 | } 35 | } 36 | } 37 | 38 | private fun createAuth(): Array { 39 | val uri = URI.create("http://gitlfs.local/test.git/info/lfs") 40 | val headers = ImmutableMap.builder() 41 | .put("Authorization", "RemoteAuth Token-" + id.incrementAndGet()) 42 | val downloadAuth = Link(uri, headers.build(), null) 43 | if (chunkedUpload) { 44 | headers.put("Transfer-Encoding", "chunked") 45 | } 46 | val uploadAuth = Link(uri, headers.build(), null) 47 | return arrayOf(downloadAuth, uploadAuth) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpRecord.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import com.google.common.base.Utf8 4 | import com.google.common.io.BaseEncoding 5 | import org.apache.http.HttpEntityEnclosingRequest 6 | import org.apache.http.HttpResponse 7 | import org.apache.http.ProtocolVersion 8 | import org.apache.http.client.methods.CloseableHttpResponse 9 | import org.apache.http.client.methods.HttpUriRequest 10 | import org.apache.http.entity.ByteArrayEntity 11 | import org.apache.http.message.BasicHttpResponse 12 | import org.apache.http.protocol.HTTP 13 | import java.io.ByteArrayOutputStream 14 | import java.nio.charset.StandardCharsets 15 | import java.util.* 16 | 17 | /** 18 | * HTTP request-response pair for testing. 19 | * 20 | * @author Artem V. Navrotskiy 21 | */ 22 | class HttpRecord { 23 | val request: Request 24 | 25 | val response: Response 26 | 27 | constructor(request: HttpUriRequest, response: HttpResponse) { 28 | this.request = Request(request) 29 | this.response = Response(response) 30 | } 31 | 32 | private constructor() { 33 | request = Request() 34 | response = Response() 35 | } 36 | 37 | class Response { 38 | private val statusCode: Int 39 | private val statusText: String 40 | private val headers: TreeMap 41 | private val body: ByteArray? 42 | 43 | internal constructor() { 44 | statusCode = 0 45 | statusText = "" 46 | headers = TreeMap() 47 | body = null 48 | } 49 | 50 | internal constructor(response: HttpResponse) { 51 | statusCode = response.statusLine.statusCode 52 | statusText = response.statusLine.reasonPhrase 53 | headers = TreeMap() 54 | for (header in response.allHeaders) { 55 | headers[header.name] = header.value 56 | } 57 | ByteArrayOutputStream().use { stream -> 58 | response.entity.writeTo(stream) 59 | body = stream.toByteArray() 60 | response.entity = ByteArrayEntity(body) 61 | } 62 | } 63 | 64 | fun toHttpResponse(): CloseableHttpResponse { 65 | val response = CloseableBasicHttpResponse(ProtocolVersion("HTTP", 1, 0), statusCode, statusText) 66 | for ((key, value) in headers) response.addHeader(key, value) 67 | if (body != null) response.entity = ByteArrayEntity(body) 68 | return response 69 | } 70 | 71 | override fun toString(): String { 72 | val sb = StringBuilder() 73 | sb.append("HTTP/1.0 ").append(statusCode).append(" ").append(statusText).append("\n") 74 | for ((key, value) in headers) { 75 | sb.append(key).append(": ").append(value).append("\n") 76 | } 77 | if (body != null) { 78 | sb.append("\n").append(asString(body)) 79 | } 80 | return sb.toString() 81 | } 82 | } 83 | 84 | private class CloseableBasicHttpResponse( 85 | ver: ProtocolVersion, 86 | code: Int, 87 | reason: String 88 | ) : BasicHttpResponse(ver, code, reason), CloseableHttpResponse { 89 | override fun close() { 90 | // noop 91 | } 92 | } 93 | 94 | class Request { 95 | private val href: String 96 | private val method: String 97 | private val headers: TreeMap 98 | private val body: ByteArray? 99 | 100 | internal constructor() { 101 | href = "" 102 | method = "" 103 | headers = TreeMap() 104 | body = null 105 | } 106 | 107 | internal constructor(request: HttpUriRequest) { 108 | href = request.uri.toString() 109 | method = request.method 110 | headers = TreeMap() 111 | val entityRequest = if (request is HttpEntityEnclosingRequest) request else null 112 | val entity = entityRequest?.entity 113 | if (entity != null) { 114 | if (entity.isChunked || entity.contentLength < 0) { 115 | request.addHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING) 116 | } else { 117 | request.addHeader(HTTP.CONTENT_LEN, entity.contentLength.toString()) 118 | } 119 | val contentType = entity.contentType 120 | if (contentType != null) { 121 | headers[contentType.name] = contentType.value 122 | } 123 | ByteArrayOutputStream().use { buffer -> 124 | entity.writeTo(buffer) 125 | body = buffer.toByteArray() 126 | } 127 | entityRequest.entity = ByteArrayEntity(body) 128 | } else { 129 | body = null 130 | } 131 | for (header in request.allHeaders) { 132 | headers[header.name] = header.value 133 | } 134 | headers.remove(HTTP.TARGET_HOST) 135 | headers.remove(HTTP.USER_AGENT) 136 | } 137 | 138 | override fun toString(): String { 139 | val sb = StringBuilder() 140 | sb.append(method).append(" ").append(href).append("\n") 141 | for ((key, value) in headers) { 142 | sb.append(key).append(": ").append(value).append("\n") 143 | } 144 | if (body != null) { 145 | sb.append("\n").append(asString(body)) 146 | } 147 | return sb.toString() 148 | } 149 | } 150 | 151 | companion object { 152 | private fun asString(data: ByteArray): String { 153 | return if (Utf8.isWellFormed(data)) { 154 | String(data, StandardCharsets.UTF_8) 155 | } else { 156 | BaseEncoding.base16().encode(data) 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpRecorder.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.apache.http.client.methods.CloseableHttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | import java.io.IOException 6 | 7 | /** 8 | * Client with recording all request. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | class HttpRecorder(private val executor: HttpExecutor) : HttpExecutor { 13 | val records = ArrayList() 14 | 15 | @Throws(IOException::class) 16 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse { 17 | val response = executor.executeMethod(request) 18 | records.add(HttpRecord(request, response)) 19 | return response 20 | } 21 | 22 | @Throws(IOException::class) 23 | override fun close() { 24 | executor.close() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/HttpReplay.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.apache.http.client.methods.CloseableHttpResponse 4 | import org.apache.http.client.methods.HttpUriRequest 5 | import org.testng.Assert 6 | import java.io.IOException 7 | import java.util.* 8 | 9 | /** 10 | * Replay recorded HTTP requests. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class HttpReplay(records: List) : HttpExecutor { 15 | private val records: Deque 16 | 17 | @Throws(IOException::class) 18 | override fun executeMethod(request: HttpUriRequest): CloseableHttpResponse { 19 | val record = records.pollFirst() 20 | Assert.assertNotNull(record) 21 | val expected = record.request.toString() 22 | val actual = HttpRecord.Request(request).toString() 23 | Assert.assertEquals(actual, expected) 24 | return record.response.toHttpResponse() 25 | } 26 | 27 | override fun close() { 28 | Assert.assertTrue(records.isEmpty()) 29 | } 30 | 31 | init { 32 | this.records = ArrayDeque(records) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/Recorder.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.apache.http.impl.client.HttpClients 4 | import ru.bozaro.gitlfs.client.AuthHelper.create 5 | import ru.bozaro.gitlfs.client.internal.HttpClientExecutor 6 | import ru.bozaro.gitlfs.common.data.BatchReq 7 | import ru.bozaro.gitlfs.common.data.Meta 8 | import ru.bozaro.gitlfs.common.data.Operation 9 | import java.io.File 10 | import java.io.FileOutputStream 11 | import java.io.IOException 12 | import java.io.OutputStreamWriter 13 | import java.nio.charset.StandardCharsets 14 | 15 | /** 16 | * Simple code for recording replay. 17 | * 18 | * @author Artem V. Navrotskiy 19 | */ 20 | object Recorder { 21 | @Throws(IOException::class) 22 | @JvmStatic 23 | fun main(args: Array) { 24 | val auth = create("git@github.com:bozaro/test.git") 25 | HttpClients.createDefault().use { httpClient -> 26 | val recorder = HttpRecorder(HttpClientExecutor(httpClient)) 27 | doWork(Client(auth, recorder)) 28 | val yaml = YamlHelper.get() 29 | val file = File("build/replay.yml") 30 | file.parentFile.mkdirs() 31 | FileOutputStream(file).use { replay -> 32 | yaml.dumpAll( 33 | recorder.records.iterator(), 34 | OutputStreamWriter(replay, StandardCharsets.UTF_8) 35 | ) 36 | } 37 | } 38 | } 39 | 40 | @Throws(IOException::class) 41 | private fun doWork(client: Client) { 42 | client.postBatch( 43 | BatchReq( 44 | Operation.Upload, 45 | listOf( 46 | Meta("b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 28), 47 | Meta("1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 3) 48 | ) 49 | ) 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlConstructor.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.yaml.snakeyaml.constructor.AbstractConstruct 4 | import org.yaml.snakeyaml.constructor.Constructor 5 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder 6 | import org.yaml.snakeyaml.nodes.Node 7 | import org.yaml.snakeyaml.nodes.ScalarNode 8 | import org.yaml.snakeyaml.nodes.Tag 9 | import java.nio.charset.StandardCharsets 10 | 11 | /** 12 | * Constructor for more user-friendly serializing request body. 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | class YamlConstructor : Constructor() { 17 | private inner class ConstructBlob : AbstractConstruct() { 18 | override fun construct(node: Node): Any { 19 | val value = constructScalar(node as ScalarNode) 20 | return value.toByteArray(StandardCharsets.UTF_8) 21 | } 22 | } 23 | 24 | private inner class ConstructBinary : AbstractConstruct() { 25 | override fun construct(node: Node): Any { 26 | val value = constructScalar(node as ScalarNode) 27 | return Base64Coder.decodeLines(value) 28 | } 29 | } 30 | 31 | init { 32 | yamlConstructors[Tag("!text")] = ConstructBlob() 33 | yamlConstructors[Tag.BINARY] = ConstructBinary() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlHelper.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import org.yaml.snakeyaml.DumperOptions 4 | import org.yaml.snakeyaml.Yaml 5 | import org.yaml.snakeyaml.introspector.BeanAccess 6 | 7 | /** 8 | * Helper for YAML. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | object YamlHelper { 13 | private val YAML = createYaml() 14 | 15 | private fun createYaml(): Yaml { 16 | val options = DumperOptions() 17 | options.lineBreak = DumperOptions.LineBreak.UNIX 18 | options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK 19 | val yaml = Yaml(YamlConstructor(), YamlRepresenter(), options) 20 | yaml.setBeanAccess(BeanAccess.FIELD) 21 | return yaml 22 | } 23 | 24 | fun createReplay(resource: String): HttpReplay { 25 | val records: MutableList = ArrayList() 26 | for (item in get().loadAll(YamlHelper::class.java.getResourceAsStream(resource))) { 27 | records.add(item as HttpRecord) 28 | } 29 | return HttpReplay(records) 30 | } 31 | 32 | fun get(): Yaml { 33 | return YAML 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/kotlin/ru/bozaro/gitlfs/client/YamlRepresenter.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.client 2 | 3 | import com.google.common.base.Utf8 4 | import org.yaml.snakeyaml.DumperOptions 5 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder 6 | import org.yaml.snakeyaml.nodes.Node 7 | import org.yaml.snakeyaml.nodes.Tag 8 | import org.yaml.snakeyaml.representer.Represent 9 | import org.yaml.snakeyaml.representer.Representer 10 | import java.nio.charset.StandardCharsets 11 | 12 | /** 13 | * Representer for more user-friendly serializing request body. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class YamlRepresenter : Representer() { 18 | private inner class RepresentBlob : Represent { 19 | override fun representData(data: Any): Node { 20 | val value = data as ByteArray 21 | return if (Utf8.isWellFormed(value)) { 22 | representScalar(Tag("!text"), String(value, StandardCharsets.UTF_8), DumperOptions.ScalarStyle.LITERAL) 23 | } else { 24 | val binary = Base64Coder.encodeLines(data) 25 | representScalar(Tag.BINARY, binary, DumperOptions.ScalarStyle.LITERAL) 26 | } 27 | } 28 | } 29 | 30 | init { 31 | representers[ByteArray::class.java] = RepresentBlob() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-download-01.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "operation" : "download", 6 | "objects" : [ { 7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 8 | "size" : 28 9 | }, { 10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 11 | "size" : 3 12 | } ] 13 | } 14 | headers: 15 | Accept: application/vnd.git-lfs+json 16 | Authorization: RemoteAuth Token-1 17 | Content-Length: '253' 18 | Content-Type: application/vnd.git-lfs+json 19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch 20 | method: POST 21 | response: 22 | body: !text |- 23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"actions":{"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"Token-2","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151007T190640Z"}}}},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"error":{"code":404,"message":"Object does not exist on the server"}}]} 24 | headers: 25 | Access-Control-Allow-Credentials: 'true' 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | Cache-Control: private, max-age=60, s-maxage=60 29 | Content-Length: '796' 30 | Content-Security-Policy: default-src 'none' 31 | Content-Type: application/json; charset=utf-8 32 | Date: Wed, 07 Oct 2015 19:06:40 GMT 33 | ETag: '"004aac442b3b84c52ed623c5c9e9f90a"' 34 | Server: GitHub.com 35 | Status: 200 OK 36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 37 | Vary: Accept-Encoding 38 | X-Content-Type-Options: nosniff 39 | X-Frame-Options: deny 40 | X-GitHub-Media-Type: unknown 41 | X-GitHub-Request-Id: 4FA5AFEC:10755:147CE471:56156D40 42 | X-RateLimit-Limit: '3000' 43 | X-RateLimit-Remaining: '2999' 44 | X-RateLimit-Reset: '1444244860' 45 | X-Served-By: 8a5c38021a5cd7cef7b8f49a296fee40 46 | X-XSS-Protection: 1; mode=block 47 | statusCode: 200 48 | statusText: OK 49 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-download-02.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "operation" : "download", 6 | "objects" : [ { 7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 8 | "size" : 28 9 | }, { 10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 11 | "size" : 3 12 | } ] 13 | } 14 | headers: 15 | Accept: application/vnd.git-lfs+json 16 | Authorization: RemoteAuth Token-1 17 | Content-Length: '253' 18 | Content-Type: application/vnd.git-lfs+json 19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch 20 | method: POST 21 | response: 22 | body: !text |- 23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"_links":{"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"Token-2","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151007T190640Z"}}}},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"error":{"code":404,"message":"Object does not exist on the server"}}]} 24 | headers: 25 | Access-Control-Allow-Credentials: 'true' 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | Cache-Control: private, max-age=60, s-maxage=60 29 | Content-Length: '796' 30 | Content-Security-Policy: default-src 'none' 31 | Content-Type: application/json; charset=utf-8 32 | Date: Wed, 07 Oct 2015 19:06:40 GMT 33 | ETag: '"004aac442b3b84c52ed623c5c9e9f90a"' 34 | Server: GitHub.com 35 | Status: 200 OK 36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 37 | Vary: Accept-Encoding 38 | X-Content-Type-Options: nosniff 39 | X-Frame-Options: deny 40 | X-GitHub-Media-Type: unknown 41 | X-GitHub-Request-Id: 4FA5AFEC:10755:147CE471:56156D40 42 | X-RateLimit-Limit: '3000' 43 | X-RateLimit-Remaining: '2999' 44 | X-RateLimit-Reset: '1444244860' 45 | X-Served-By: 8a5c38021a5cd7cef7b8f49a296fee40 46 | X-XSS-Protection: 1; mode=block 47 | statusCode: 200 48 | statusText: OK 49 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-01.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "operation" : "upload", 6 | "objects" : [ { 7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 8 | "size" : 28 9 | }, { 10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 11 | "size" : 3 12 | } ] 13 | } 14 | headers: 15 | Accept: application/vnd.git-lfs+json 16 | Authorization: RemoteAuth Token-1 17 | Content-Length: '251' 18 | Content-Type: application/vnd.git-lfs+json 19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch 20 | method: POST 21 | response: 22 | body: !text |- 23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"actions":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]} 24 | headers: 25 | Access-Control-Allow-Credentials: 'true' 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | Cache-Control: private, max-age=60, s-maxage=60 29 | Content-Length: '989' 30 | Content-Security-Policy: default-src 'none' 31 | Content-Type: application/json; charset=utf-8 32 | Date: Wed, 07 Oct 2015 19:07:30 GMT 33 | ETag: '"4061c2b43579394fce28f86aaeef3502"' 34 | Server: GitHub.com 35 | Status: 200 OK 36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 37 | Vary: Accept-Encoding 38 | X-Content-Type-Options: nosniff 39 | X-Frame-Options: deny 40 | X-GitHub-Media-Type: unknown 41 | X-RateLimit-Limit: '3000' 42 | X-RateLimit-Remaining: '2998' 43 | X-RateLimit-Reset: '1444244860' 44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290 45 | X-XSS-Protection: 1; mode=block 46 | statusCode: 200 47 | statusText: OK 48 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-02.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "operation" : "upload", 6 | "objects" : [ { 7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 8 | "size" : 28 9 | }, { 10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 11 | "size" : 3 12 | } ] 13 | } 14 | headers: 15 | Accept: application/vnd.git-lfs+json 16 | Authorization: RemoteAuth Token-1 17 | Content-Length: '251' 18 | Content-Type: application/vnd.git-lfs+json 19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch 20 | method: POST 21 | response: 22 | body: !text |- 23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]} 24 | headers: 25 | Access-Control-Allow-Credentials: 'true' 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | Cache-Control: private, max-age=60, s-maxage=60 29 | Content-Length: '989' 30 | Content-Security-Policy: default-src 'none' 31 | Content-Type: application/json; charset=utf-8 32 | Date: Wed, 07 Oct 2015 19:07:30 GMT 33 | ETag: '"4061c2b43579394fce28f86aaeef3502"' 34 | Server: GitHub.com 35 | Status: 200 OK 36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 37 | Vary: Accept-Encoding 38 | X-Content-Type-Options: nosniff 39 | X-Frame-Options: deny 40 | X-GitHub-Media-Type: unknown 41 | X-RateLimit-Limit: '3000' 42 | X-RateLimit-Remaining: '2998' 43 | X-RateLimit-Reset: '1444244860' 44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290 45 | X-XSS-Protection: 1; mode=block 46 | statusCode: 200 47 | statusText: OK 48 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/batch-upload-chunked.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "operation" : "upload", 6 | "objects" : [ { 7 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 8 | "size" : 28 9 | }, { 10 | "oid" : "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa", 11 | "size" : 3 12 | } ] 13 | } 14 | headers: 15 | Accept: application/vnd.git-lfs+json 16 | Authorization: RemoteAuth Token-1 17 | Content-Type: application/vnd.git-lfs+json 18 | Transfer-Encoding: chunked 19 | href: http://gitlfs.local/test.git/info/lfs/objects/batch 20 | method: POST 21 | response: 22 | body: !text |- 23 | {"objects":[{"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28},{"oid":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","size":3,"actions":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/1c/be/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=Token-2","x-amz-content-sha256":"1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa","x-amz-date":"20151007T190730Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa/verify","header":{"Authorization":"RemoteAuth Token-3","Accept":"application/vnd.git-lfs+json"}}}}]} 24 | headers: 25 | Access-Control-Allow-Credentials: 'true' 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 28 | Cache-Control: private, max-age=60, s-maxage=60 29 | Content-Length: '989' 30 | Content-Security-Policy: default-src 'none' 31 | Content-Type: application/json; charset=utf-8 32 | Date: Wed, 07 Oct 2015 19:07:30 GMT 33 | ETag: '"4061c2b43579394fce28f86aaeef3502"' 34 | Server: GitHub.com 35 | Status: 200 OK 36 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 37 | Vary: Accept-Encoding 38 | X-Content-Type-Options: nosniff 39 | X-Frame-Options: deny 40 | X-GitHub-Media-Type: unknown 41 | X-RateLimit-Limit: '3000' 42 | X-RateLimit-Remaining: '2998' 43 | X-RateLimit-Reset: '1444244860' 44 | X-Served-By: a7f8a126c9ed3f1c4715a34c0ddc7290 45 | X-XSS-Protection: 1; mode=block 46 | statusCode: 200 47 | statusText: OK 48 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-download-01.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: null 4 | headers: 5 | Accept: application/vnd.git-lfs+json 6 | Authorization: RemoteAuth Token-1 7 | href: http://gitlfs.local/test.git/info/lfs/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e 8 | method: GET 9 | response: 10 | body: !text |- 11 | {"oid":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","size":28,"_links":{"self":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e"},"download":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9","x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amz-date":"20151002T185112Z"}}}} 12 | headers: 13 | Accept: application/vnd.git-lfs+json 14 | Authorization: RemoteAuth Token-1 15 | Host: api.github.com 16 | User-Agent: Jakarta Commons-HttpClient/3.1 17 | statusCode: 200 18 | statusText: OK 19 | --- !!ru.bozaro.gitlfs.client.HttpRecord 20 | request: 21 | body: null 22 | headers: 23 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9 24 | x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 25 | x-amz-date: 20151002T185112Z 26 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138 27 | method: GET 28 | response: 29 | body: !text |- 30 | Fri Oct 02 21:07:33 MSK 2015 31 | headers: 32 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=4f1ee7f7294be783a6243cfb01bf524bdfd292af8183ce313360c7d0b3dc2ed9 33 | Host: github-cloud.s3.amazonaws.com 34 | User-Agent: Jakarta Commons-HttpClient/3.1 35 | x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 36 | x-amz-date: 20151002T185112Z 37 | statusCode: 200 38 | statusText: OK 39 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-download-02.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: null 4 | headers: 5 | Accept: application/vnd.git-lfs+json 6 | Authorization: RemoteAuth Token-1 7 | href: http://gitlfs.local/test.git/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b 8 | method: GET 9 | response: 10 | body: !text |- 11 | {"message":"Not Found","documentation_url":"https://github.com/contact"} 12 | headers: 13 | Access-Control-Allow-Credentials: 'true' 14 | Access-Control-Allow-Origin: '*' 15 | Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 16 | Content-Length: '72' 17 | Content-Security-Policy: default-src 'none' 18 | Content-Type: application/json; charset=utf-8 19 | Date: Wed, 07 Oct 2015 04:06:04 GMT 20 | Server: GitHub.com 21 | Status: 404 Not Found 22 | Strict-Transport-Security: max-age=31536000; includeSubdomains; preload 23 | X-Content-Type-Options: nosniff 24 | X-Frame-Options: deny 25 | X-GitHub-Media-Type: unknown 26 | X-GitHub-Request-Id: 4FA5AFEC:17F56:60D2406:56149A2C 27 | X-RateLimit-Limit: '3000' 28 | X-RateLimit-Remaining: '2999' 29 | X-RateLimit-Reset: '1444190824' 30 | X-XSS-Protection: 1; mode=block 31 | statusCode: 404 32 | statusText: Not Found 33 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-01.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 6 | "size" : 28 7 | } 8 | headers: 9 | Accept: application/vnd.git-lfs+json 10 | Authorization: RemoteAuth Token-1 11 | Content-Length: '95' 12 | Content-Type: application/vnd.git-lfs+json 13 | href: http://gitlfs.local/test.git/info/lfs/objects 14 | method: POST 15 | response: 16 | body: !text |- 17 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0","x-amz-content-sha256":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","x-amz-date":"20151002T180739Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify","header":{"Authorization":"RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==","Accept":"application/vnd.git-lfs+json"}}}} 18 | headers: 19 | Accept: application/vnd.git-lfs+json 20 | Authorization: RemoteAuth 40a488f3-dd0e-4f61-862b-10350d273b7e 21 | Content-Length: '95' 22 | Content-Type: application/vnd.git-lfs+json 23 | Host: api.github.com 24 | User-Agent: Jakarta Commons-HttpClient/3.1 25 | statusCode: 202 26 | statusText: Accepted 27 | --- !!ru.bozaro.gitlfs.client.HttpRecord 28 | request: 29 | body: !text |- 30 | Fri Oct 02 21:07:33 MSK 2015 31 | headers: 32 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0 33 | Content-Length: '28' 34 | Content-Type: application/octet-stream 35 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e 36 | x-amz-date: 20151002T180739Z 37 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138 38 | method: PUT 39 | response: 40 | body: !text "" 41 | headers: 42 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0 43 | Content-Length: '28' 44 | Content-Type: application/octet-stream 45 | Host: github-cloud.s3.amazonaws.com 46 | User-Agent: Jakarta Commons-HttpClient/3.1 47 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e 48 | x-amz-date: 20151002T180739Z 49 | statusCode: 200 50 | statusText: OK 51 | --- !!ru.bozaro.gitlfs.client.HttpRecord 52 | request: 53 | body: !text |- 54 | { 55 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 56 | "size" : 28 57 | } 58 | headers: 59 | Accept: application/vnd.git-lfs+json 60 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA== 61 | Content-Length: '95' 62 | Content-Type: application/vnd.git-lfs+json 63 | href: https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify 64 | method: POST 65 | response: 66 | body: !text |- 67 | {} 68 | headers: 69 | Accept: application/vnd.git-lfs+json 70 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA== 71 | Content-Length: '95' 72 | Content-Type: application/vnd.git-lfs+json 73 | Host: api.github.com 74 | User-Agent: Jakarta Commons-HttpClient/3.1 75 | statusCode: 200 76 | statusText: OK 77 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-02.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 6 | "size" : 28 7 | } 8 | headers: 9 | Accept: application/vnd.git-lfs+json 10 | Authorization: RemoteAuth Token-1 11 | Content-Length: '95' 12 | Content-Type: application/vnd.git-lfs+json 13 | href: http://gitlfs.local/test.git/info/lfs/objects 14 | method: POST 15 | response: 16 | body: !text |- 17 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"} 18 | headers: 19 | Accept: application/vnd.git-lfs+json 20 | Authorization: RemoteAuth Token-1 21 | Content-Length: '95' 22 | Content-Type: application/vnd.git-lfs+json 23 | Host: api.github.com 24 | User-Agent: Jakarta Commons-HttpClient/3.1 25 | statusCode: 403 26 | statusText: Forbidden 27 | --- !!ru.bozaro.gitlfs.client.HttpRecord 28 | request: 29 | body: !text |- 30 | { 31 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 32 | "size" : 28 33 | } 34 | headers: 35 | Accept: application/vnd.git-lfs+json 36 | Authorization: RemoteAuth Token-2 37 | Content-Length: '95' 38 | Content-Type: application/vnd.git-lfs+json 39 | href: http://gitlfs.local/test.git/info/lfs/objects 40 | method: POST 41 | response: 42 | body: !text |- 43 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"} 44 | headers: 45 | Accept: application/vnd.git-lfs+json 46 | Authorization: RemoteAuth Token-2 47 | Content-Length: '95' 48 | Content-Type: application/vnd.git-lfs+json 49 | Host: api.github.com 50 | User-Agent: Jakarta Commons-HttpClient/3.1 51 | statusCode: 403 52 | statusText: Forbidden 53 | --- !!ru.bozaro.gitlfs.client.HttpRecord 54 | request: 55 | body: !text |- 56 | { 57 | "oid" : "e6ba93fef485f9cb6748286d3ad610123bcf37f6ceb8a0f847a5d4f20ed31af3", 58 | "size" : 28 59 | } 60 | headers: 61 | Accept: application/vnd.git-lfs+json 62 | Authorization: RemoteAuth Token-3 63 | Content-Length: '95' 64 | Content-Type: application/vnd.git-lfs+json 65 | href: https://api.github.com/lfs/bozaro/test/objects 66 | method: POST 67 | response: 68 | body: !text |- 69 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"} 70 | headers: 71 | Accept: application/vnd.git-lfs+json 72 | Authorization: RemoteAuth Token-3 73 | Content-Length: '95' 74 | Content-Type: application/vnd.git-lfs+json 75 | Host: api.github.com 76 | User-Agent: Jakarta Commons-HttpClient/3.1 77 | statusCode: 403 78 | statusText: Forbidden 79 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-03.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 6 | "size" : 28 7 | } 8 | headers: 9 | Accept: application/vnd.git-lfs+json 10 | Authorization: RemoteAuth Token-1 11 | Content-Length: '95' 12 | Content-Type: application/vnd.git-lfs+json 13 | href: http://gitlfs.local/test.git/info/lfs/objects 14 | method: POST 15 | response: 16 | body: !text |- 17 | {"message":"You need Push access to upload Git LFS objects.","documentation_url":"https://github.com/contact"} 18 | headers: 19 | Accept: application/vnd.git-lfs+json 20 | Authorization: RemoteAuth Token-1 21 | Content-Length: '95' 22 | Content-Type: application/vnd.git-lfs+json 23 | Host: api.github.com 24 | User-Agent: Jakarta Commons-HttpClient/3.1 25 | statusCode: 403 26 | statusText: Forbidden 27 | --- !!ru.bozaro.gitlfs.client.HttpRecord 28 | request: 29 | body: !text |- 30 | { 31 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 32 | "size" : 28 33 | } 34 | headers: 35 | Accept: application/vnd.git-lfs+json 36 | Authorization: RemoteAuth Token-2 37 | Content-Length: '95' 38 | Content-Type: application/vnd.git-lfs+json 39 | href: http://gitlfs.local/test.git/info/lfs/objects 40 | method: POST 41 | response: 42 | body: !text |- 43 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0","x-amz-content-sha256":"b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e","x-amz-date":"20151002T180739Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify","header":{"Authorization":"RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA==","Accept":"application/vnd.git-lfs+json"}}}} 44 | headers: 45 | Accept: application/vnd.git-lfs+json 46 | Authorization: RemoteAuth 40a488f3-dd0e-4f61-862b-10350d273b7e 47 | Content-Length: '95' 48 | Content-Type: application/vnd.git-lfs+json 49 | Host: api.github.com 50 | User-Agent: Jakarta Commons-HttpClient/3.1 51 | statusCode: 202 52 | statusText: Accepted 53 | --- !!ru.bozaro.gitlfs.client.HttpRecord 54 | request: 55 | body: !text |- 56 | Fri Oct 02 21:07:33 MSK 2015 57 | headers: 58 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0 59 | Content-Length: '28' 60 | Content-Type: application/octet-stream 61 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e 62 | x-amz-date: 20151002T180739Z 63 | href: https://github-cloud.s3.amazonaws.com/alambic/media/111975537/b8/10/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e?actor_id=2458138 64 | method: PUT 65 | response: 66 | body: !text "" 67 | headers: 68 | Authorization: AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151002/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=e13e0b8615cd2e56b547895f8a9fd6b42ed2f04d38d451d406142293d6706df0 69 | Content-Length: '28' 70 | Content-Type: application/octet-stream 71 | Host: github-cloud.s3.amazonaws.com 72 | User-Agent: Jakarta Commons-HttpClient/3.1 73 | x-amz-content-sha256: b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e 74 | x-amz-date: 20151002T180739Z 75 | statusCode: 200 76 | statusText: OK 77 | --- !!ru.bozaro.gitlfs.client.HttpRecord 78 | request: 79 | body: !text |- 80 | { 81 | "oid" : "b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e", 82 | "size" : 28 83 | } 84 | headers: 85 | Accept: application/vnd.git-lfs+json 86 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA== 87 | Content-Length: '95' 88 | Content-Type: application/vnd.git-lfs+json 89 | href: https://api.github.com/lfs/bozaro/test/objects/b810bbe954d51e380f395de0c301a0a42d16f115453f2feb4188ca9f7189074e/verify 90 | method: POST 91 | response: 92 | body: !text |- 93 | {} 94 | headers: 95 | Accept: application/vnd.git-lfs+json 96 | Authorization: RemoteAuth ACWCGu7nufaX-64PlRGCIN7NT0Rdd1Pyks5WEBlrwA== 97 | Content-Length: '95' 98 | Content-Type: application/vnd.git-lfs+json 99 | Host: api.github.com 100 | User-Agent: Jakarta Commons-HttpClient/3.1 101 | statusCode: 200 102 | statusText: OK 103 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/legacy-upload-04.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "oid" : "61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308", 6 | "size" : 15 7 | } 8 | headers: 9 | Accept: application/vnd.git-lfs+json 10 | Authorization: RemoteAuth Token-1 11 | Content-Length: '95' 12 | Content-Type: application/vnd.git-lfs+json 13 | href: http://gitlfs.local/test.git/info/lfs/objects 14 | method: POST 15 | response: 16 | body: !text |- 17 | {"_links":{"upload":{"href":"https://github-cloud.s3.amazonaws.com/alambic/media/111975537/61/f2/61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308?actor_id=2458138","header":{"Authorization":"AWS4-HMAC-SHA256 Credential=AKIAIMWPLRQEC4XCWWPA/20151003/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=21bf6f6e7c966d514de3c6cc6cfaa5ffcba218337658674b7f0889337b7f11c7","x-amz-content-sha256":"61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308","x-amz-date":"20151003T072433Z"}},"verify":{"href":"https://api.github.com/lfs/bozaro/test/objects/61f27ddd5b4e533246eb76c45ed4bf4504daabce12589f97b3285e9d3cd54308/verify","header":{"Authorization":"RemoteAuth ACWCGjaSQ5a9uvMqIQ9ZGyaphLde-j3sks5WENQxwA==","Accept":"application/vnd.git-lfs+json"}}}} 18 | headers: 19 | Accept: application/vnd.git-lfs+json 20 | Authorization: RemoteAuth 07ff548d-8797-442e-bedf-b03f8c967091 21 | Content-Length: '95' 22 | Content-Type: application/vnd.git-lfs+json 23 | Host: api.github.com 24 | User-Agent: Jakarta Commons-HttpClient/3.1 25 | statusCode: 200 26 | statusText: OK 27 | -------------------------------------------------------------------------------- /gitlfs-client/src/test/resources/ru/bozaro/gitlfs/client/locking-01.yml: -------------------------------------------------------------------------------- 1 | !!ru.bozaro.gitlfs.client.HttpRecord 2 | request: 3 | body: !text |- 4 | { 5 | "path" : "build.gradle", 6 | "ref" : { 7 | "name" : "refs/heads/master" 8 | } 9 | } 10 | headers: 11 | Accept: application/vnd.git-lfs+json 12 | Authorization: RemoteAuth Token-1 13 | Content-Length: '79' 14 | Content-Type: application/vnd.git-lfs+json 15 | href: http://gitlfs.local/test.git/info/lfs/locks 16 | method: POST 17 | response: 18 | body: !text | 19 | {"lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:27.933646398Z"}} 20 | headers: 21 | Content-Length: '92' 22 | Content-Type: application/vnd.git-lfs+json 23 | Date: Wed, 15 May 2019 09:56:27 GMT 24 | X-GitHub-Request-Id: D6AC:50EC:37071C:6CF18D:5CDBE24B 25 | statusCode: 201 26 | statusText: Created 27 | --- !!ru.bozaro.gitlfs.client.HttpRecord 28 | request: 29 | body: !text |- 30 | { 31 | "path" : "build.gradle", 32 | "ref" : { 33 | "name" : "refs/heads/master" 34 | } 35 | } 36 | headers: 37 | Accept: application/vnd.git-lfs+json 38 | Authorization: RemoteAuth Token-1 39 | Content-Length: '79' 40 | Content-Type: application/vnd.git-lfs+json 41 | href: http://gitlfs.local/test.git/info/lfs/locks 42 | method: POST 43 | response: 44 | body: !text | 45 | {"documentation_url":"https://github.com/contact","lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:28Z"},"message":"Lock exists","request_id":"D6AC:50EC:370726:6CF1AA:5CDBE24B"} 46 | headers: 47 | Content-Length: '203' 48 | Content-Type: application/vnd.git-lfs+json 49 | Date: Wed, 15 May 2019 09:56:28 GMT 50 | X-GitHub-Request-Id: D6AC:50EC:370726:6CF1AA:5CDBE24B 51 | statusCode: 409 52 | statusText: Conflict 53 | --- !!ru.bozaro.gitlfs.client.HttpRecord 54 | request: 55 | body: null 56 | headers: 57 | Accept: application/vnd.git-lfs+json 58 | Authorization: RemoteAuth Token-1 59 | href: http://gitlfs.local/test.git/info/lfs/locks?path=build.gradle&refspec=refs%2Fheads%2Fmaster 60 | method: GET 61 | response: 62 | body: !text | 63 | {"locks":[{"id":"107214","path":"build.gradle","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:56:28Z"}],"next_cursor":""} 64 | headers: 65 | Content-Length: '134' 66 | Content-Type: application/vnd.git-lfs+json 67 | Date: Wed, 15 May 2019 09:56:28 GMT 68 | X-GitHub-Request-Id: D6AC:50EC:370759:6CF1CF:5CDBE24C 69 | statusCode: 200 70 | statusText: OK 71 | --- !!ru.bozaro.gitlfs.client.HttpRecord 72 | request: 73 | body: !text |- 74 | { 75 | "ref" : { 76 | "name" : "refs/heads/master" 77 | } 78 | } 79 | headers: 80 | Accept: application/vnd.git-lfs+json 81 | Authorization: RemoteAuth Token-1 82 | Content-Length: '52' 83 | Content-Type: application/vnd.git-lfs+json 84 | href: http://gitlfs.local/test.git/info/lfs/locks/verify 85 | method: POST 86 | response: 87 | body: !text | 88 | {"ours":[{"id":"107193","path":"README.md","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:21:33Z"},{"id":"107214","path":"build.gradle","owner":{"name":"slonopotamus"},"locked_at":"2019-05-15T09:56:28Z"}],"theirs":[],"next_cursor":""} 89 | headers: 90 | Content-Length: '247' 91 | Content-Type: application/vnd.git-lfs+json 92 | Date: Wed, 15 May 2019 09:56:28 GMT 93 | X-GitHub-Request-Id: D6AC:50EC:370768:6CF22D:5CDBE24C 94 | statusCode: 200 95 | statusText: OK 96 | --- !!ru.bozaro.gitlfs.client.HttpRecord 97 | request: 98 | body: !text |- 99 | { 100 | "force" : true, 101 | "ref" : { 102 | "name" : "refs/heads/master" 103 | } 104 | } 105 | headers: 106 | Accept: application/vnd.git-lfs+json 107 | Authorization: RemoteAuth Token-1 108 | Content-Length: '70' 109 | Content-Type: application/vnd.git-lfs+json 110 | href: http://gitlfs.local/test.git/info/lfs/locks/107214/unlock 111 | method: POST 112 | response: 113 | body: !text | 114 | {"lock":{"id":"107214","path":"build.gradle","locked_at":"2019-05-15T09:56:28Z","unlocked_at":"2019-05-15T09:56:29.09800075Z"}} 115 | headers: 116 | Content-Length: '128' 117 | Content-Type: application/vnd.git-lfs+json 118 | Date: Wed, 15 May 2019 09:56:29 GMT 119 | X-GitHub-Request-Id: D6AC:50EC:370779:6CF25A:5CDBE24C 120 | statusCode: 200 121 | statusText: OK 122 | --- !!ru.bozaro.gitlfs.client.HttpRecord 123 | request: 124 | body: !text |- 125 | { 126 | "force" : false, 127 | "ref" : { 128 | "name" : "refs/heads/master" 129 | } 130 | } 131 | headers: 132 | Accept: application/vnd.git-lfs+json 133 | Authorization: RemoteAuth Token-1 134 | Content-Length: '71' 135 | Content-Type: application/vnd.git-lfs+json 136 | href: http://gitlfs.local/test.git/info/lfs/locks/107214/unlock 137 | method: POST 138 | response: 139 | body: !text | 140 | {"documentation_url":"https://github.com/contact","message":"Lock not found","request_id":"D6AC:50EC:370786:6CF26F:5CDBE24D"} 141 | headers: 142 | Content-Length: '126' 143 | Content-Type: application/vnd.git-lfs+json 144 | Date: Wed, 15 May 2019 09:56:29 GMT 145 | X-GitHub-Request-Id: D6AC:50EC:370786:6CF26F:5CDBE24D 146 | statusCode: 404 147 | statusText: Not Found 148 | -------------------------------------------------------------------------------- /gitlfs-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Java Git-LFS common structures" 2 | 3 | dependencies { 4 | api("com.fasterxml.jackson.core:jackson-databind:2.16.1") 5 | } 6 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common 2 | 3 | /** 4 | * Git-lfs constants. 5 | * 6 | * @author Artem V. Navrotskiy 7 | */ 8 | object Constants { 9 | /** 10 | * See [][HTTP/1.1 documentation"><a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">HTTP/1.1 documentation</a>. 11 | */ 12 | const val HEADER_AUTHORIZATION = "Authorization" 13 | 14 | const val HEADER_ACCEPT = "Accept" 15 | 16 | const val HEADER_LOCATION = "Location" 17 | 18 | const val MIME_LFS_JSON = "application/vnd.git-lfs+json" 19 | 20 | const val MIME_BINARY = "application/octet-stream" 21 | 22 | const val PATH_OBJECTS = "objects" 23 | 24 | const val PATH_BATCH = "objects/batch" 25 | 26 | const val PATH_LOCKS = "locks" 27 | 28 | /** 29 | * Minimal supported batch size. 30 | */ 31 | const val BATCH_SIZE = 100 32 | } 33 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/JsonHelper.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import com.fasterxml.jackson.core.util.DefaultIndenter 5 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter 6 | import com.fasterxml.jackson.databind.DeserializationFeature 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | import com.fasterxml.jackson.databind.SerializationFeature 9 | import com.fasterxml.jackson.databind.util.StdDateFormat 10 | import java.text.DateFormat 11 | 12 | /** 13 | * Json utility class. 14 | * 15 | * @author Artem V. Navrotskiy 16 | * @author Marat Radchenko @slonopotamus.org> 17 | */ 18 | object JsonHelper { 19 | /** 20 | * git-lfs cannot parse timezone without colon: [com.fasterxml.jackson.databind.util.StdDateFormat]. 21 | * 22 | * 23 | * See https://github.com/git-lfs/git-lfs/issues/3660 24 | */ 25 | val dateFormat: DateFormat = StdDateFormat.instance.withColonInTimeZone(true) 26 | 27 | val mapper = ObjectMapper() 28 | 29 | init { 30 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 31 | mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 32 | mapper.enable(SerializationFeature.INDENT_OUTPUT) 33 | mapper.dateFormat = dateFormat 34 | 35 | // By default, pretty printer uses system newline. Explicitly configure it to use \n 36 | mapper.setDefaultPrettyPrinter( 37 | DefaultPrettyPrinter(DefaultPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR) 38 | .withObjectIndenter(DefaultIndenter(" ", "\n"))) 39 | mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/LockConflictException.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common 2 | 3 | import ru.bozaro.gitlfs.common.data.Lock 4 | 5 | class LockConflictException(message: String?, val lock: Lock) : Exception(message) { 6 | 7 | constructor(lock: Lock) : this("Lock exists", lock) 8 | } 9 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/VerifyLocksResult.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common 2 | 3 | import ru.bozaro.gitlfs.common.data.Lock 4 | 5 | class VerifyLocksResult(val ourLocks: List, val theirLocks: List) 6 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchItem.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import java.util.* 5 | 6 | /** 7 | * LFS batch object. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | class BatchItem( 12 | @JsonProperty(value = "oid", required = true) oid: String, 13 | @JsonProperty(value = "size", required = true) size: Long, 14 | @JsonProperty(value = "actions") links1: Map?, 15 | @JsonProperty(value = "_links") links2: Map?, 16 | @field:JsonProperty(value = "error") 17 | @param:JsonProperty(value = "error") 18 | val error: Error? 19 | ) : Meta(oid, size), Links { 20 | @JsonProperty(value = "actions") 21 | override val links: Map = combine(links1, links2) 22 | 23 | constructor(meta: Meta, links: MutableMap) : this(meta.oid, meta.size, links, null, null) 24 | constructor(meta: Meta, error: Error?) : this(meta.oid, meta.size, null, null, error) 25 | 26 | companion object { 27 | private fun combine(a: Map?, b: Map?): Map { 28 | var r: Map? = null 29 | if (a != null && a.isNotEmpty()) { 30 | r = a 31 | } 32 | if (b != null && b.isNotEmpty()) { 33 | if (r == null) { 34 | r = b 35 | } else { 36 | r = TreeMap(r) 37 | r.putAll(b) 38 | } 39 | } 40 | return if (r == null) emptyMap() else Collections.unmodifiableMap(r) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchReq.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import java.util.* 5 | 6 | /** 7 | * Batch request. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | class BatchReq( 12 | @field:JsonProperty(value = "operation", required = true) 13 | @param:JsonProperty(value = "operation", required = true) 14 | val operation: Operation, 15 | @JsonProperty(value = "objects", required = true) objects: List 16 | ) { 17 | 18 | @JsonProperty(value = "objects", required = true) 19 | val objects: List = Collections.unmodifiableList(ArrayList(objects)) 20 | } 21 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/BatchRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import java.util.* 6 | 7 | /** 8 | * Batch request. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | class BatchRes @JsonCreator constructor( 13 | @JsonProperty(value = "objects", required = true) objects: List 14 | ) { 15 | @JsonProperty(value = "objects", required = true) 16 | val objects: List = Collections.unmodifiableList(ArrayList(objects)) 17 | } 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/CreateLockReq.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class CreateLockReq( 6 | /** 7 | * String path name of the locked file. 8 | */ 9 | @field:JsonProperty(value = "path", required = true) 10 | @param:JsonProperty(value = "path", required = true) val path: String, 11 | /** 12 | * Optional object describing the server ref that the locks belong to. 13 | */ 14 | @field:JsonProperty(value = "ref") 15 | @param:JsonProperty(value = "ref") 16 | val ref: Ref? 17 | ) 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/CreateLockRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class CreateLockRes( 6 | @field:JsonProperty(value = "lock", required = true) 7 | @param:JsonProperty(value = "lock", required = true) 8 | val lock: Lock 9 | ) 10 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockReq.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class DeleteLockReq( 6 | /** 7 | * Optional boolean specifying that the user is deleting another user's lock. 8 | */ 9 | @field:JsonProperty(value = "force") @param:JsonProperty(value = "force") private val force: Boolean?, 10 | /** 11 | * Optional object describing the server ref that the locks belong to. 12 | */ 13 | @field:JsonProperty(value = "ref") @param:JsonProperty(value = "ref") val ref: Ref?) { 14 | fun isForce(): Boolean { 15 | return force != null && force 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class DeleteLockRes( 6 | @field:JsonProperty(value = "lock", required = true) 7 | @param:JsonProperty(value = "lock", required = true) 8 | val lock: Lock 9 | ) 10 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Error.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | 6 | /** 7 | * LFS error description. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | class Error @JsonCreator constructor( 12 | @field:JsonProperty(value = "code", required = true) @param:JsonProperty(value = "code") val code: Int, 13 | @field:JsonProperty(value = "message") @param:JsonProperty(value = "message") val message: String? 14 | ) 15 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Link.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import java.net.URI 6 | import java.util.* 7 | 8 | /** 9 | * LFS reference. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | class Link @JsonCreator constructor( 14 | @field:JsonProperty(value = "href", required = true) 15 | @param:JsonProperty(value = "href", required = true) 16 | val href: URI, 17 | @JsonProperty("header") header: Map?, 18 | @JsonProperty("expires_at") expiresAt: Date? 19 | ) { 20 | 21 | @JsonProperty("header") 22 | val header: Map = header?.let { TreeMap(it) } ?: emptyMap() 23 | 24 | @JsonProperty("expires_at") 25 | val expiresAt: Date? = if (expiresAt != null) Date(expiresAt.time) else null 26 | } 27 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LinkType.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonValue 5 | 6 | /** 7 | * LFSP operation type. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | enum class LinkType { 12 | Download { 13 | override fun toValue(): String { 14 | return "download" 15 | } 16 | 17 | override fun visit(visitor: Visitor): R { 18 | return visitor.visitDownload() 19 | } 20 | }, 21 | Upload { 22 | override fun toValue(): String { 23 | return "upload" 24 | } 25 | 26 | override fun visit(visitor: Visitor): R { 27 | return visitor.visitUpload() 28 | } 29 | }, 30 | Verify { 31 | override fun toValue(): String { 32 | return "verify" 33 | } 34 | 35 | override fun visit(visitor: Visitor): R { 36 | return visitor.visitVerify() 37 | } 38 | }, 39 | Self { 40 | override fun toValue(): String { 41 | return "self" 42 | } 43 | 44 | override fun visit(visitor: Visitor): R { 45 | return visitor.visitSelf() 46 | } 47 | }; 48 | 49 | @JsonValue 50 | abstract fun toValue(): String 51 | abstract fun visit(visitor: Visitor): R 52 | interface Visitor { 53 | fun visitDownload(): R 54 | fun visitUpload(): R 55 | fun visitVerify(): R 56 | fun visitSelf(): R 57 | } 58 | 59 | companion object { 60 | @JsonCreator 61 | fun forValue(value: String): LinkType? { 62 | for (item in values()) { 63 | if (item.toValue() == value) { 64 | return item 65 | } 66 | } 67 | return null 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Links.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | /** 4 | * Object locations. 5 | * 6 | * @author Artem V. Navrotskiy 7 | */ 8 | interface Links { 9 | val links: Map 10 | } 11 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Lock.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import java.util.* 5 | 6 | class Lock( 7 | /** 8 | * String ID of the Lock. 9 | */ 10 | @field:JsonProperty(value = "id", required = true) 11 | @param:JsonProperty(value = "id", required = true) 12 | val id: String, 13 | /** 14 | * String path name of the locked file. 15 | */ 16 | @field:JsonProperty(value = "path", required = true) 17 | @param:JsonProperty(value = "path", required = true) 18 | val path: String, 19 | /** 20 | * The timestamp the lock was created, as an ISO 8601 formatted string. 21 | */ 22 | @field:JsonProperty(value = "locked_at", required = true) 23 | @param:JsonProperty(value = "locked_at") 24 | val lockedAt: Date, 25 | /** 26 | * The name of the user that created the Lock. This should be set from the user credentials posted when creating the lock. 27 | */ 28 | @field:JsonProperty(value = "owner") 29 | @param:JsonProperty(value = "owner") 30 | val owner: User? 31 | ) : Comparable { 32 | 33 | override fun hashCode(): Int { 34 | return Objects.hash(id) 35 | } 36 | 37 | override fun equals(other: Any?): Boolean { 38 | return if (other !is Lock) false else id == other.id 39 | } 40 | 41 | override fun compareTo(other: Lock): Int { 42 | return lockComparator.compare(this, other) 43 | } 44 | 45 | companion object { 46 | val lockComparator: Comparator = Comparator 47 | .comparing { obj: Lock -> obj.lockedAt } 48 | .thenComparing { obj: Lock -> obj.id } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LockConflictRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | 6 | class LockConflictRes @JsonCreator constructor( 7 | @field:JsonProperty(value = "message", required = true) 8 | @param:JsonProperty(value = "message", required = true) 9 | val message: String, 10 | @field:JsonProperty(value = "lock", required = true) 11 | @param:JsonProperty(value = "lock", required = true) 12 | val lock: Lock 13 | ) 14 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/LocksRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class LocksRes( 6 | @field:JsonProperty(value = "locks", required = true) 7 | @param:JsonProperty(value = "locks", required = true) 8 | val locks: List, 9 | @field:JsonProperty(value = "next_cursor") 10 | @param:JsonProperty(value = "next_cursor") 11 | val nextCursor: String? 12 | ) 13 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Meta.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import java.util.* 6 | 7 | /** 8 | * LFS object location. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | open class Meta @JsonCreator constructor( 13 | @field:JsonProperty(value = "oid", required = true) 14 | @param:JsonProperty(value = "oid", required = true) val oid: String, 15 | @field:JsonProperty(value = "size", required = true) 16 | @param:JsonProperty(value = "size", required = true) val size: Long 17 | ) { 18 | 19 | override fun hashCode(): Int { 20 | return Objects.hash(oid, size) 21 | } 22 | 23 | override fun equals(other: Any?): Boolean { 24 | if (other !is Meta) return false 25 | return size == other.size && oid == other.oid 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/ObjectRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | import java.util.* 6 | 7 | /** 8 | * LFS object location. 9 | * 10 | * @author Artem V. Navrotskiy 11 | */ 12 | class ObjectRes @JsonCreator constructor( 13 | @JsonProperty(value = "oid") oid: String?, 14 | @JsonProperty(value = "size") size: Long, 15 | @JsonProperty(value = "_links", required = true) links: Map 16 | ) : Links { 17 | @JsonProperty(value = "_links", required = true) 18 | override val links: Map = Collections.unmodifiableMap(TreeMap(links)) 19 | val meta: Meta? = oid?.let { Meta(it, size) } 20 | } 21 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Operation.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonValue 5 | 6 | /** 7 | * LFSP operation type. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | enum class Operation { 12 | Download { 13 | override fun toValue(): String { 14 | return "download" 15 | } 16 | 17 | override fun visit(visitor: Visitor): R { 18 | return visitor.visitDownload() 19 | } 20 | }, 21 | Upload { 22 | override fun toValue(): String { 23 | return "upload" 24 | } 25 | 26 | override fun visit(visitor: Visitor): R { 27 | return visitor.visitUpload() 28 | } 29 | }; 30 | 31 | @JsonValue 32 | abstract fun toValue(): String 33 | abstract fun visit(visitor: Visitor): R 34 | interface Visitor { 35 | fun visitDownload(): R 36 | fun visitUpload(): R 37 | } 38 | 39 | companion object { 40 | @JsonCreator 41 | fun forValue(value: String): Operation? { 42 | for (item in values()) { 43 | if (item.toValue() == value) { 44 | return item 45 | } 46 | } 47 | return null 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/Ref.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class Ref( 6 | /** 7 | * Fully-qualified server refspec. 8 | */ 9 | @field:JsonProperty(value = "name", required = true) 10 | @param:JsonProperty(value = "name", required = true) 11 | val name: String) { 12 | override fun toString(): String { 13 | return name 14 | } 15 | 16 | companion object { 17 | fun create(ref: String?): Ref? { 18 | return if (ref == null) null else Ref(ref) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/User.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class User( 6 | @field:JsonProperty(value = "name", required = true) 7 | @param:JsonProperty(value = "name", required = true) 8 | val name: String 9 | ) 10 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksReq.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class VerifyLocksReq( 6 | /** 7 | * Optional cursor to allow pagination. 8 | */ 9 | @field:JsonProperty(value = "cursor") @param:JsonProperty(value = "cursor") val cursor: String?, 10 | /** 11 | * Optional object describing the server ref that the locks belong to. 12 | */ 13 | @field:JsonProperty(value = "ref") @param:JsonProperty(value = "ref") val ref: Ref?, 14 | /** 15 | * Optional limit to how many locks to return. 16 | */ 17 | @field:JsonProperty(value = "limit") @param:JsonProperty(value = "limit") val limit: Int?) 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksRes.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | class VerifyLocksRes( 6 | @field:JsonProperty(value = "ours", required = true) 7 | @param:JsonProperty(value = "ours", required = true) 8 | val ours: List, 9 | @field:JsonProperty(value = "theirs", required = true) 10 | @param:JsonProperty(value = "theirs", required = true) 11 | val theirs: List, 12 | @field:JsonProperty(value = "next_cursor") 13 | @param:JsonProperty(value = "next_cursor") 14 | val nextCursor: String? 15 | ) 16 | -------------------------------------------------------------------------------- /gitlfs-common/src/main/kotlin/ru/bozaro/gitlfs/common/io/InputStreamValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.io 2 | 3 | import ru.bozaro.gitlfs.common.data.Meta 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.security.MessageDigest 7 | import java.security.NoSuchAlgorithmException 8 | 9 | /** 10 | * Wrapper for validating hash and size of uploading object. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class InputStreamValidator(stream: InputStream, meta: Meta) : InputStream() { 15 | private var digest: MessageDigest? = null 16 | 17 | private val stream: InputStream 18 | 19 | private val meta: Meta 20 | private var eof: Boolean 21 | private var totalSize: Long 22 | 23 | @Throws(IOException::class) 24 | override fun read(): Int { 25 | if (eof) { 26 | return -1 27 | } 28 | val data = stream.read() 29 | if (data >= 0) { 30 | digest!!.update(data.toByte()) 31 | checkSize(1) 32 | } else { 33 | checkSize(-1) 34 | } 35 | return data 36 | } 37 | 38 | @Throws(IOException::class) 39 | private fun checkSize(size: Int) { 40 | if (size > 0) { 41 | totalSize += size.toLong() 42 | } 43 | if (meta.size in 1 until totalSize) { 44 | throw IOException("Input stream too big") 45 | } 46 | if (size < 0) { 47 | eof = true 48 | if (meta.size >= 0 && totalSize != meta.size) { 49 | throw IOException("Unexpected end of stream") 50 | } 51 | val hash = toHexString(digest!!.digest()) 52 | if (meta.oid != hash) { 53 | throw IOException("Invalid stream hash") 54 | } 55 | } 56 | } 57 | 58 | @Throws(IOException::class) 59 | override fun read(buffer: ByteArray, off: Int, len: Int): Int { 60 | if (eof) { 61 | return -1 62 | } 63 | val size = stream.read(buffer, off, len) 64 | if (size > 0) { 65 | digest!!.update(buffer, off, size) 66 | } 67 | checkSize(size) 68 | return size 69 | } 70 | 71 | @Throws(IOException::class) 72 | override fun close() { 73 | stream.close() 74 | } 75 | 76 | companion object { 77 | private val hexDigits = "0123456789abcdef".toCharArray() 78 | private fun toHexString(bytes: ByteArray): String { 79 | val sb = StringBuilder(2 * bytes.size) 80 | for (b in bytes) { 81 | sb.append(hexDigits[b.toInt() shr 4 and 0xf]).append(hexDigits[b.toInt() and 0xf]) 82 | } 83 | return sb.toString() 84 | } 85 | } 86 | 87 | init { 88 | try { 89 | digest = MessageDigest.getInstance("SHA-256") 90 | } catch (e: NoSuchAlgorithmException) { 91 | throw IOException(e) 92 | } 93 | this.stream = stream 94 | this.meta = meta 95 | eof = false 96 | totalSize = 0 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/BatchReqTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import java.io.IOException 6 | import java.net.URISyntaxException 7 | import java.text.ParseException 8 | 9 | /** 10 | * Test Meta deserialization. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class BatchReqTest { 15 | @Test 16 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 17 | fun parse01() { 18 | val data: BatchReq = SerializeTester.deserialize("batch-req-01.json", BatchReq::class.java) 19 | Assert.assertNotNull(data) 20 | Assert.assertEquals(data.operation, Operation.Upload) 21 | Assert.assertEquals(1, data.objects.size) 22 | val meta: Meta = data.objects[0] 23 | Assert.assertNotNull(meta) 24 | Assert.assertEquals(meta.oid, "1111111") 25 | Assert.assertEquals(meta.size, 123L) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/BatchResTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import com.google.common.collect.ImmutableMap 5 | import org.testng.Assert 6 | import org.testng.annotations.Test 7 | import java.io.IOException 8 | import java.net.URI 9 | import java.net.URISyntaxException 10 | import java.text.ParseException 11 | 12 | /** 13 | * Test Meta deserialization. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class BatchResTest { 18 | @Test 19 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 20 | fun parse01() { 21 | val data: BatchRes = SerializeTester.deserialize("batch-res-01.json", BatchRes::class.java) 22 | Assert.assertNotNull(data) 23 | Assert.assertEquals(2, data.objects.size) 24 | run { 25 | val item: BatchItem = data.objects[0] 26 | Assert.assertNotNull(item) 27 | Assert.assertEquals(item.oid, "1111111") 28 | Assert.assertEquals(item.size, 123L) 29 | Assert.assertNull(item.error) 30 | Assert.assertEquals(1, item.links.size) 31 | val link: Link = item.links[LinkType.Download]!! 32 | Assert.assertNotNull(link) 33 | Assert.assertEquals(link.href, URI("https://some-download.com")) 34 | Assert.assertEquals( 35 | link.header, 36 | ImmutableMap.builder() 37 | .put("Authorization", "Basic ...") 38 | .build() 39 | ) 40 | Assert.assertEquals(link.expiresAt, StdDateFormat.instance.parse("2015-07-27T21:15:01.000+00:00")) 41 | } 42 | run { 43 | val item: BatchItem = data.objects[1] 44 | Assert.assertNotNull(item) 45 | Assert.assertEquals(item.oid, "2222222") 46 | Assert.assertEquals(item.size, 234L) 47 | Assert.assertTrue(item.links.isEmpty()) 48 | val error = item.error!! 49 | Assert.assertNotNull(error) 50 | Assert.assertEquals(error.code, 404) 51 | Assert.assertEquals(error.message, "Object does not exist on the server") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/CreateLockReqTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import java.io.IOException 6 | 7 | class CreateLockReqTest { 8 | @Test 9 | @Throws(IOException::class) 10 | fun parse01() { 11 | val data: CreateLockReq = SerializeTester.deserialize("create-lock-req-01.json", CreateLockReq::class.java) 12 | Assert.assertNotNull(data) 13 | Assert.assertEquals(data.path, "foo/bar.zip") 14 | Assert.assertNotNull(data.ref) 15 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/CreateLockResTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import java.io.IOException 7 | import java.text.ParseException 8 | 9 | class CreateLockResTest { 10 | @Test 11 | @Throws(IOException::class, ParseException::class) 12 | fun parse01() { 13 | val data: CreateLockRes = SerializeTester.deserialize("create-lock-res-01.json", CreateLockRes::class.java) 14 | Assert.assertNotNull(data) 15 | val lock = data.lock 16 | Assert.assertNotNull(lock) 17 | Assert.assertEquals(lock.id, "some-uuid") 18 | Assert.assertEquals(lock.path, "/path/to/file") 19 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00")) 20 | Assert.assertNotNull(lock.owner) 21 | Assert.assertEquals(lock.owner!!.name, "Jane Doe") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/DateTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import ru.bozaro.gitlfs.common.JsonHelper 6 | import java.text.ParseException 7 | 8 | class DateTest { 9 | @Test 10 | @Throws(ParseException::class) 11 | fun format() { 12 | val str = "2006-01-02T15:04:05.123+00:00" 13 | Assert.assertEquals(JsonHelper.dateFormat.format(JsonHelper.dateFormat.parse(str)), str) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/DeleteLockReqTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import java.io.IOException 6 | 7 | class DeleteLockReqTest { 8 | @Test 9 | @Throws(IOException::class) 10 | fun parse01() { 11 | val data: DeleteLockReq = SerializeTester.deserialize("delete-lock-req-01.json", DeleteLockReq::class.java) 12 | Assert.assertNotNull(data) 13 | Assert.assertTrue(data.isForce()) 14 | Assert.assertNotNull(data.ref) 15 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/LinkTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import com.google.common.collect.ImmutableMap 5 | import org.testng.Assert 6 | import org.testng.annotations.Test 7 | import java.io.IOException 8 | import java.net.URI 9 | import java.net.URISyntaxException 10 | import java.text.ParseException 11 | 12 | /** 13 | * Test Link deserialization. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class LinkTest { 18 | @Test 19 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 20 | fun parse01() { 21 | val link: Link = SerializeTester.deserialize("link-01.json", Link::class.java) 22 | Assert.assertNotNull(link) 23 | Assert.assertEquals(link.href, URI("https://storage-server.com/OID")) 24 | Assert.assertEquals(link.header, 25 | ImmutableMap.builder() 26 | .put("Authorization", "Basic ...") 27 | .build() 28 | ) 29 | } 30 | 31 | @Test 32 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 33 | fun parse02() { 34 | val link: Link = SerializeTester.deserialize("link-02.json", Link::class.java) 35 | Assert.assertNotNull(link) 36 | Assert.assertEquals(link.href, URI("https://api.github.com/lfs/bozaro/git-lfs-java")) 37 | Assert.assertEquals(link.header, 38 | ImmutableMap.builder() 39 | .put("Authorization", "RemoteAuth secret") 40 | .build() 41 | ) 42 | Assert.assertEquals(link.expiresAt, StdDateFormat.instance.parse("2015-09-17T19:17:31.000+00:00")) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/LocksResTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import java.io.IOException 7 | import java.text.ParseException 8 | 9 | class LocksResTest { 10 | @Test 11 | @Throws(IOException::class, ParseException::class) 12 | fun parse01() { 13 | val data: LocksRes = SerializeTester.deserialize("locks-res-01.json", LocksRes::class.java) 14 | Assert.assertNotNull(data) 15 | Assert.assertEquals(data.nextCursor, "optional next ID") 16 | Assert.assertEquals(data.locks.size, 1) 17 | val lock: Lock = data.locks[0] 18 | Assert.assertNotNull(lock) 19 | Assert.assertEquals(lock.id, "some-uuid") 20 | Assert.assertEquals(lock.path, "/path/to/file") 21 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00")) 22 | Assert.assertNotNull(lock.owner) 23 | Assert.assertEquals(lock.owner!!.name, "Jane Doe") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/MetaTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import java.io.IOException 6 | import java.net.URISyntaxException 7 | import java.text.ParseException 8 | 9 | /** 10 | * Test Meta deserialization. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | class MetaTest { 15 | @Test 16 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 17 | fun parse01() { 18 | val meta: Meta = SerializeTester.deserialize("meta-01.json", Meta::class.java) 19 | Assert.assertNotNull(meta) 20 | Assert.assertEquals(meta.oid, "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b") 21 | Assert.assertEquals(meta.size, 130L) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/ObjectResTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import java.io.IOException 7 | import java.net.URI 8 | import java.net.URISyntaxException 9 | import java.text.ParseException 10 | 11 | /** 12 | * Test Meta deserialization. 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | class ObjectResTest { 17 | @Test 18 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 19 | fun parse01() { 20 | val res: ObjectRes = SerializeTester.deserialize("object-res-01.json", ObjectRes::class.java) 21 | Assert.assertNotNull(res) 22 | val meta = res.meta!! 23 | Assert.assertNotNull(meta) 24 | Assert.assertEquals(meta.oid, "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b") 25 | Assert.assertEquals(meta.size, 130L) 26 | Assert.assertEquals(2, res.links.size) 27 | val self: Link = res.links[LinkType.Self]!! 28 | Assert.assertNotNull(self) 29 | Assert.assertEquals(self.href, URI("https://storage-server.com/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b")) 30 | Assert.assertTrue(self.header.isEmpty()) 31 | val link: Link = res.links[LinkType.Upload]!! 32 | Assert.assertNotNull(link) 33 | Assert.assertEquals(link.href, URI("https://storage-server.com/OID")) 34 | Assert.assertEquals(link.header, 35 | ImmutableMap.builder() 36 | .put("Authorization", "Basic ...") 37 | .build() 38 | ) 39 | } 40 | 41 | @Test 42 | @Throws(IOException::class, ParseException::class, URISyntaxException::class) 43 | fun parse02() { 44 | val res: ObjectRes = SerializeTester.deserialize("object-res-02.json", ObjectRes::class.java) 45 | Assert.assertNotNull(res) 46 | Assert.assertNull(res.meta) 47 | Assert.assertEquals(1, res.links.size) 48 | val link: Link = res.links[LinkType.Upload]!! 49 | Assert.assertNotNull(link) 50 | Assert.assertEquals(link.href, URI("https://some-upload.com")) 51 | Assert.assertEquals(link.header, 52 | ImmutableMap.builder() 53 | .put("Authorization", "Basic ...") 54 | .build() 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/SerializeTester.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import ru.bozaro.gitlfs.common.JsonHelper 5 | 6 | /** 7 | * Class for searialization tester. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | object SerializeTester { 12 | fun deserialize(path: String?, type: Class?): T { 13 | SerializeTester::class.java.getResourceAsStream(path).use { stream -> 14 | Assert.assertNotNull(stream) 15 | val value: T = JsonHelper.mapper.readValue(stream, type) 16 | Assert.assertNotNull(value) 17 | val json: String = JsonHelper.mapper.writeValueAsString(value) 18 | return JsonHelper.mapper.readValue(json, type) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksReqTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import java.io.IOException 6 | 7 | class VerifyLocksReqTest { 8 | @Test 9 | @Throws(IOException::class) 10 | fun parse01() { 11 | val data: VerifyLocksReq = SerializeTester.deserialize("verify-locks-req-01.json", VerifyLocksReq::class.java) 12 | Assert.assertNotNull(data) 13 | Assert.assertEquals(data.cursor, "optional cursor") 14 | Assert.assertEquals(100, data.limit) 15 | Assert.assertNotNull(data.ref) 16 | Assert.assertEquals(data.ref!!.name, "refs/heads/my-feature") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/kotlin/ru/bozaro/gitlfs/common/data/VerifyLocksResTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.common.data 2 | 3 | import com.fasterxml.jackson.databind.util.StdDateFormat 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import java.io.IOException 7 | import java.text.ParseException 8 | 9 | class VerifyLocksResTest { 10 | @Test 11 | @Throws(IOException::class, ParseException::class) 12 | fun parse01() { 13 | val data: VerifyLocksRes = SerializeTester.deserialize("verify-locks-res-01.json", VerifyLocksRes::class.java) 14 | Assert.assertNotNull(data) 15 | Assert.assertEquals(data.nextCursor, "optional next ID") 16 | Assert.assertEquals(data.ours.size, 1) 17 | val lock = data.ours[0] 18 | Assert.assertEquals(lock.id, "some-uuid") 19 | Assert.assertEquals(lock.path, "/path/to/file") 20 | Assert.assertEquals(lock.lockedAt, StdDateFormat.instance.parse("2016-05-17T15:49:06+00:00")) 21 | Assert.assertNotNull(lock.owner) 22 | Assert.assertEquals(lock.owner!!.name, "Jane Doe") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/batch-req-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "operation": "upload", 3 | "objects": [ 4 | { 5 | "oid": "1111111", 6 | "size": 123 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/batch-res-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "objects": [ 3 | { 4 | "oid": "1111111", 5 | "size": 123, 6 | "actions": { 7 | "download": { 8 | "href": "https://some-download.com", 9 | "header": { 10 | "Authorization": "Basic ..." 11 | }, 12 | "expires_at": "2015-07-27T21:15:01Z" 13 | } 14 | } 15 | }, 16 | { 17 | "oid": "2222222", 18 | "size": 234, 19 | "error": { 20 | "code": 404, 21 | "message": "Object does not exist on the server" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/create-lock-req-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "foo/bar.zip", 3 | "ref": { 4 | "name": "refs/heads/my-feature" 5 | } 6 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/create-lock-res-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": { 3 | "id": "some-uuid", 4 | "path": "/path/to/file", 5 | "locked_at": "2016-05-17T15:49:06+00:00", 6 | "owner": { 7 | "name": "Jane Doe" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/delete-lock-req-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "force": true, 3 | "ref": { 4 | "name": "refs/heads/my-feature" 5 | } 6 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/link-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://storage-server.com/OID", 3 | "header": { 4 | "Authorization": "Basic ..." 5 | } 6 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/link-02.json: -------------------------------------------------------------------------------- 1 | { 2 | "href": "https://api.github.com/lfs/bozaro/git-lfs-java", 3 | "header": { 4 | "Authorization": "RemoteAuth secret" 5 | }, 6 | "expires_at": "2015-09-17T19:17:31Z" 7 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/locks-res-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "locks": [ 3 | { 4 | "id": "some-uuid", 5 | "path": "/path/to/file", 6 | "locked_at": "2016-05-17T15:49:06+00:00", 7 | "owner": { 8 | "name": "Jane Doe" 9 | } 10 | } 11 | ], 12 | "next_cursor": "optional next ID" 13 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/meta-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "oid": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", 3 | "size": 130 4 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/object-res-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "oid": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", 3 | "size": 130, 4 | "_links": { 5 | "self": { 6 | "href": "https://storage-server.com/info/lfs/objects/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" 7 | }, 8 | "upload": { 9 | "href": "https://storage-server.com/OID", 10 | "header": { 11 | "Authorization": "Basic ..." 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/object-res-02.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links": { 3 | "upload": { 4 | "href": "https://some-upload.com", 5 | "header": { 6 | "Authorization": "Basic ..." 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/verify-locks-req-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "cursor": "optional cursor", 3 | "limit": 100, 4 | "ref": { 5 | "name": "refs/heads/my-feature" 6 | } 7 | } -------------------------------------------------------------------------------- /gitlfs-common/src/test/resources/ru/bozaro/gitlfs/common/data/verify-locks-res-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "ours": [ 3 | { 4 | "id": "some-uuid", 5 | "path": "/path/to/file", 6 | "locked_at": "2016-05-17T15:49:06+00:00", 7 | "owner": { 8 | "name": "Jane Doe" 9 | } 10 | } 11 | ], 12 | "theirs": [], 13 | "next_cursor": "optional next ID" 14 | } -------------------------------------------------------------------------------- /gitlfs-pointer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Java Git-LFS pointer manipulation" 2 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/main/kotlin/ru/bozaro/gitlfs/pointer/Constants.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.pointer 2 | 3 | /** 4 | * Git-lfs constants. 5 | * 6 | * @author Artem V. Navrotskiy 7 | */ 8 | object Constants { 9 | const val POINTER_MAX_SIZE = 1024 10 | 11 | const val VERSION_URL = "https://git-lfs.github.com/spec/v1" 12 | 13 | const val OID = "oid" 14 | 15 | const val SIZE = "size" 16 | 17 | const val VERSION = "version" 18 | } 19 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/main/kotlin/ru/bozaro/gitlfs/pointer/Pointer.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.pointer 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | import java.nio.ByteBuffer 6 | import java.nio.charset.StandardCharsets 7 | import java.util.* 8 | import java.util.regex.Pattern 9 | 10 | /** 11 | * Class for read/writer pointer blobs. 12 | * https://github.com/github/git-lfs/blob/master/docs/spec.md 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | object Pointer { 17 | private val PREFIX = (Constants.VERSION + ' ').toByteArray(StandardCharsets.UTF_8) 18 | 19 | private val REQUIRED = arrayOf( 20 | RequiredKey(Constants.OID, Pattern.compile("^[0-9a-z]+:[0-9a-f]+$")), 21 | RequiredKey(Constants.SIZE, Pattern.compile("^\\d+$")) 22 | ) 23 | 24 | /** 25 | * Serialize pointer map. 26 | * 27 | * @param pointer Pointer ru.bozaro.gitlfs.common.data. 28 | * @return Pointer content bytes. 29 | */ 30 | fun serializePointer(pointer: Map): ByteArray { 31 | val data: MutableMap = TreeMap(pointer) 32 | val buffer = StringBuilder() 33 | // Write version. 34 | run { 35 | var version = data.remove(Constants.VERSION) 36 | if (version == null) { 37 | version = Constants.VERSION_URL 38 | } 39 | buffer.append(Constants.VERSION).append(' ').append(version).append('\n') 40 | } 41 | for ((key, value) in data) { 42 | buffer.append(key).append(' ').append(value).append('\n') 43 | } 44 | return buffer.toString().toByteArray(StandardCharsets.UTF_8) 45 | } 46 | 47 | /** 48 | * Create pointer with oid and size. 49 | * 50 | * @param oid Object oid. 51 | * @param size Object size. 52 | * @return Return pointer ru.bozaro.gitlfs.common.data. 53 | */ 54 | fun createPointer(oid: String, size: Long): Map { 55 | val pointer = TreeMap() 56 | pointer[Constants.VERSION] = Constants.VERSION_URL 57 | pointer[Constants.OID] = oid 58 | pointer[Constants.SIZE] = size.toString() 59 | return pointer 60 | } 61 | 62 | /** 63 | * Read pointer ru.bozaro.gitlfs.common.data. 64 | * 65 | * @param stream Input stream. 66 | * @return Return pointer info or null if blob is not a pointer ru.bozaro.gitlfs.common.data. 67 | */ 68 | @Throws(IOException::class) 69 | fun parsePointer(stream: InputStream): Map? { 70 | val buffer = ByteArray(Constants.POINTER_MAX_SIZE) 71 | var size = 0 72 | while (size < buffer.size) { 73 | val len = stream.read(buffer, size, buffer.size - size) 74 | if (len <= 0) { 75 | return parsePointer(buffer, 0, size) 76 | } 77 | size += len 78 | } 79 | return null 80 | } 81 | 82 | /** 83 | * Read @{link ru.bozaro.gitlfs.common.data.Pointer} 84 | * 85 | * @return Return pointer info or null if blob is not a {@link ru.bozaro.gitlfs.common.data.Pointer} 86 | */ 87 | fun parsePointer(blob: ByteArray, offset: Int = 0, length: Int = blob.size): Map? { 88 | // Check prefix 89 | if (length < PREFIX.size) return null 90 | for (i in PREFIX.indices) { 91 | if (blob[i] != PREFIX[i]) return null 92 | } 93 | // Reading key value pairs 94 | val result = TreeMap() 95 | val decoder = StandardCharsets.UTF_8.newDecoder() 96 | var lastKey: String? = null 97 | var keyOffset = offset 98 | var required = 0 99 | while (keyOffset < length) { 100 | var valueOffset = keyOffset 101 | // Key 102 | while (true) { 103 | valueOffset++ 104 | if (valueOffset < length) { 105 | val c = blob[valueOffset] 106 | if (c == ' '.code.toByte()) break 107 | // Keys MUST only use the characters [a-z] [0-9] . -. 108 | if (c >= 'a'.code.toByte() && c <= 'z'.code.toByte()) continue 109 | if (c >= '0'.code.toByte() && c <= '9'.code.toByte()) continue 110 | if (c == '.'.code.toByte() || c == '-'.code.toByte()) continue 111 | } 112 | // Found invalid character. 113 | return null 114 | } 115 | var endOffset = valueOffset 116 | // Value 117 | do { 118 | endOffset++ 119 | if (endOffset >= length) return null 120 | // Values MUST NOT contain return or newline characters. 121 | } while (blob[endOffset] != '\n'.code.toByte()) 122 | val key = String(blob, keyOffset, valueOffset - keyOffset, StandardCharsets.UTF_8) 123 | val value: String = try { 124 | decoder.decode(ByteBuffer.wrap(blob, valueOffset + 1, endOffset - valueOffset - 1)).toString() 125 | } catch (e: CharacterCodingException) { 126 | return null 127 | } 128 | if (required < REQUIRED.size && REQUIRED[required].name == key) { 129 | if (!REQUIRED[required].pattern.matcher(value).matches()) { 130 | return null 131 | } 132 | required++ 133 | } 134 | if (keyOffset > offset) { 135 | if (lastKey != null && key <= lastKey) { 136 | return null 137 | } 138 | lastKey = key 139 | } 140 | if (result.put(key, value) != null) { 141 | return null 142 | } 143 | keyOffset = endOffset + 1 144 | } 145 | // Not found all required fields. 146 | return if (required < REQUIRED.size) { 147 | null 148 | } else result 149 | } 150 | 151 | private class RequiredKey( 152 | val name: String, 153 | val pattern: Pattern 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/kotlin/ru/bozaro/gitlfs/pointer/PointerTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.pointer 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import org.testng.Assert 5 | import org.testng.annotations.DataProvider 6 | import org.testng.annotations.Test 7 | import ru.bozaro.gitlfs.pointer.Pointer.createPointer 8 | import ru.bozaro.gitlfs.pointer.Pointer.parsePointer 9 | import ru.bozaro.gitlfs.pointer.Pointer.serializePointer 10 | import java.io.IOException 11 | 12 | /** 13 | * Pointer parser test. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class PointerTest { 18 | @Test(dataProvider = "parseValidProvider") 19 | @Throws(IOException::class) 20 | fun parseValid(fileName: String, expected: Map) { 21 | javaClass.getResourceAsStream(fileName).use { stream -> 22 | Assert.assertEquals(parsePointer(stream!!), expected) 23 | } 24 | } 25 | 26 | @Test 27 | fun parseAndSerialize() { 28 | val pointer = createPointer("sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393", 12345) 29 | Assert.assertEquals(pointer["oid"], "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") 30 | Assert.assertEquals(pointer["size"], "12345") 31 | Assert.assertEquals(pointer.size, 3) 32 | val bytes = serializePointer(pointer) 33 | val parsed: Map? = parsePointer(bytes) 34 | Assert.assertEquals(parsed, pointer) 35 | } 36 | 37 | @Test(dataProvider = "parseInvalidProvider") 38 | @Throws(IOException::class) 39 | fun parseInvalid(fileName: String, description: String) { 40 | javaClass.getResourceAsStream(fileName).use { stream -> 41 | Assert.assertNotNull(stream) 42 | Assert.assertNull(parsePointer(stream!!), description) 43 | } 44 | } 45 | 46 | @DataProvider(name = "parseValidProvider") 47 | fun parseValidProvider(): Array> { 48 | return arrayOf( 49 | arrayOf( 50 | "pointer-valid-01.dat", 51 | ImmutableMap.builder() 52 | .put("version", "https://git-lfs.github.com/spec/v1") 53 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") 54 | .put("size", "12345") 55 | .build() 56 | ), arrayOf( 57 | "pointer-valid-02.dat", 58 | ImmutableMap.builder() 59 | .put("version", "https://git-lfs.github.com/spec/v1") 60 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") 61 | .put("name", "Текст в UTF-8") 62 | .put("size", "12345") 63 | .build() 64 | ), arrayOf( 65 | "pointer-valid-03.dat", 66 | ImmutableMap.builder() 67 | .put("version", "https://git-lfs.github.com/spec/v1") 68 | .put("object-name", " Foo") 69 | .put("object.id", "F1") 70 | .put("object0123456789", ":)") 71 | .put("oid", "sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393") 72 | .put("name", "Текст в UTF-8") 73 | .put("size", "12345") 74 | .build() 75 | ) 76 | ) 77 | } 78 | 79 | @DataProvider(name = "parseInvalidProvider") 80 | fun parseInvalidProvider(): Array> { 81 | return arrayOf( 82 | arrayOf("pointer-invalid-01.dat", "Version is not in first line"), 83 | arrayOf("pointer-invalid-02.dat", "Two empty lines at end of file"), 84 | arrayOf("pointer-invalid-03.dat", "Size not found"), 85 | arrayOf("pointer-invalid-04.dat", "Oid not found"), 86 | arrayOf("pointer-invalid-05.dat", "Version not found"), 87 | arrayOf("pointer-invalid-06.dat", "Invalid items order"), 88 | arrayOf("pointer-invalid-07.dat", "Non utf-8"), 89 | arrayOf("pointer-invalid-08.dat", "Size is not number"), 90 | arrayOf("pointer-invalid-09.dat", "Duplicate line"), 91 | arrayOf("pointer-invalid-10.dat", "Duplicate version") 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-01.dat: -------------------------------------------------------------------------------- 1 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 2 | version https://git-lfs.github.com/spec/v1 3 | size 12345 4 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-02.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 3 | size 12345 4 | 5 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-03.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 3 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-04.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | size 12345 3 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-05.dat: -------------------------------------------------------------------------------- 1 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 2 | size 12345 3 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-06.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | size 12345 3 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 4 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-07.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-as-svn/git-lfs-java/e262811ab6421aaa4a1f31ba24410ce24a2544f4/gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-07.dat -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-08.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 3 | size 12o45 4 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-09.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | name 12345 3 | name 12345 4 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 5 | size 12345 6 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-invalid-10.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 3 | size 12345 4 | version https://git-lfs.github.com/spec/v1 5 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-01.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 3 | size 12345 4 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-02.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | name Текст в UTF-8 3 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 4 | size 12345 5 | -------------------------------------------------------------------------------- /gitlfs-pointer/src/test/resources/ru/bozaro/gitlfs/pointer/pointer-valid-03.dat: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | name Текст в UTF-8 3 | object-name Foo 4 | object.id F1 5 | object0123456789 :) 6 | oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 7 | size 12345 8 | -------------------------------------------------------------------------------- /gitlfs-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Java Git-LFS server implementation-free library" 2 | 3 | dependencies { 4 | api(project(":gitlfs-common")) 5 | api("org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2") 6 | 7 | testImplementation(project(":gitlfs-client")) 8 | testImplementation("org.eclipse.jetty:jetty-servlet:11.0.19") 9 | testRuntimeOnly("org.slf4j:slf4j-simple:1.7.36") 10 | } 11 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ContentManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import ru.bozaro.gitlfs.common.data.Meta 5 | import java.io.IOException 6 | import java.io.InputStream 7 | 8 | /** 9 | * Interface for store object content. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | interface ContentManager { 14 | /** 15 | * Check access for requested operation and return some user information. 16 | * 17 | * @param request HTTP request. 18 | * @return Object for send object. 19 | */ 20 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 21 | fun checkDownloadAccess(request: HttpServletRequest): Downloader 22 | 23 | /** 24 | * Check access for requested operation and return some user information. 25 | * 26 | * @param request HTTP request. 27 | * @return Object for receive object. 28 | */ 29 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 30 | fun checkUploadAccess(request: HttpServletRequest): Uploader 31 | 32 | /** 33 | * Get metadata of uploaded object. 34 | * 35 | * @param hash Object metadata (hash and size). 36 | * @return Return metadata of uploaded object. 37 | */ 38 | @Throws(IOException::class) 39 | fun getMetadata(hash: String): Meta? 40 | 41 | interface HeaderProvider { 42 | /** 43 | * Generate pointer header information (for example: replace transit Basic auth by Toker auth). 44 | * 45 | * @param header Default header. Can be modified. 46 | * @return Pointer header information. 47 | */ 48 | fun createHeader(header: Map): Map { 49 | return header 50 | } 51 | } 52 | 53 | interface Downloader : HeaderProvider { 54 | /** 55 | * Get object from storage. 56 | * 57 | * @param hash Object metadata (hash and size). 58 | * @return Return object stream. 59 | */ 60 | @Throws(IOException::class) 61 | fun openObject(hash: String): InputStream 62 | 63 | /** 64 | * Get gzip-compressed object from storage. 65 | * 66 | * @param hash Object metadata (hash and size). 67 | * @return Return gzip-compressed object stream. If gzip-stream is not available return null. 68 | */ 69 | @Throws(IOException::class) 70 | fun openObjectGzipped(hash: String): InputStream? 71 | } 72 | 73 | interface Uploader : HeaderProvider { 74 | /** 75 | * Save object to storage. 76 | * 77 | * @param meta Object metadata (hash and size). 78 | * @param content Stream with object ru.bozaro.gitlfs.common.data. 79 | */ 80 | @Throws(IOException::class) 81 | fun saveObject(meta: Meta, content: InputStream) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ContentServlet.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.ServletException 4 | import jakarta.servlet.http.HttpServlet 5 | import jakarta.servlet.http.HttpServletRequest 6 | import jakarta.servlet.http.HttpServletResponse 7 | import ru.bozaro.gitlfs.common.Constants 8 | import ru.bozaro.gitlfs.common.JsonHelper 9 | import ru.bozaro.gitlfs.common.data.Meta 10 | import ru.bozaro.gitlfs.common.io.InputStreamValidator 11 | import ru.bozaro.gitlfs.server.internal.ObjectResponse 12 | import ru.bozaro.gitlfs.server.internal.ResponseWriter 13 | import java.io.IOException 14 | import java.util.regex.Pattern 15 | 16 | /** 17 | * Servlet for content storage. 18 | * 19 | * @author Artem V. Navrotskiy 20 | */ 21 | class ContentServlet(private val manager: ContentManager) : HttpServlet() { 22 | @Throws(ServletException::class, IOException::class) 23 | override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { 24 | try { 25 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) { 26 | processGet(req, req.pathInfo.substring(1)).write(resp) 27 | return 28 | } 29 | } catch (e: ServerError) { 30 | PointerServlet.sendError(resp, e) 31 | return 32 | } 33 | super.doGet(req, resp) 34 | } 35 | 36 | companion object { 37 | val PATTERN_OID: Pattern = Pattern.compile("^/[0-9a-f]{64}$") 38 | } 39 | 40 | @Throws(IOException::class, ServletException::class) 41 | override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { 42 | try { 43 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) { 44 | processObjectVerify(req, req.pathInfo.substring(1)).write(resp) 45 | return 46 | } 47 | } catch (e: ServerError) { 48 | PointerServlet.sendError(resp, e) 49 | return 50 | } 51 | super.doPost(req, resp) 52 | } 53 | 54 | @Throws(IOException::class, ServerError::class) 55 | private fun processObjectVerify(req: HttpServletRequest, oid: String): ResponseWriter { 56 | manager.checkUploadAccess(req) 57 | val expectedMeta = JsonHelper.mapper.readValue(req.inputStream, Meta::class.java) 58 | val actualMeta = manager.getMetadata(oid) 59 | if (!expectedMeta.equals(actualMeta)) throw ServerError( 60 | HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 61 | String.format("LFS verification failure: server=%s client=%s", expectedMeta, actualMeta), 62 | null 63 | ) 64 | return ResponseWriter { response: HttpServletResponse -> response.status = HttpServletResponse.SC_OK } 65 | } 66 | 67 | @Throws(ServletException::class, IOException::class) 68 | override fun doPut(req: HttpServletRequest, resp: HttpServletResponse) { 69 | try { 70 | if (req.pathInfo != null && PATTERN_OID.matcher(req.pathInfo).matches()) { 71 | processPut(req, req.pathInfo.substring(1)).write(resp) 72 | return 73 | } 74 | } catch (e: ServerError) { 75 | PointerServlet.sendError(resp, e) 76 | return 77 | } 78 | super.doPut(req, resp) 79 | } 80 | 81 | @Throws(ServerError::class, IOException::class) 82 | private fun processPut(req: HttpServletRequest, oid: String): ResponseWriter { 83 | val uploader = manager.checkUploadAccess(req) 84 | val meta = Meta(oid, -1) 85 | uploader.saveObject(meta, InputStreamValidator(req.inputStream, meta)) 86 | return ObjectResponse(HttpServletResponse.SC_OK, meta) 87 | } 88 | 89 | @Throws(ServerError::class, IOException::class) 90 | private fun processGet(req: HttpServletRequest, oid: String): ResponseWriter { 91 | val downloader = manager.checkDownloadAccess(req) 92 | val stream = downloader.openObject(oid) 93 | return ResponseWriter { response: HttpServletResponse -> 94 | response.status = HttpServletResponse.SC_OK 95 | response.contentType = Constants.MIME_BINARY 96 | stream.use { stream -> 97 | val buffer = ByteArray(0x10000) 98 | while (true) { 99 | val read = stream.read(buffer) 100 | if (read < 0) break 101 | response.outputStream.write(buffer, 0, read) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ForbiddenError.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletResponse 4 | 5 | /** 6 | * Forbidden error. 7 | * 8 | * @author Artem V. Navrotskiy 9 | */ 10 | class ForbiddenError constructor(message: String = "Access forbidden") : 11 | ServerError(HttpServletResponse.SC_FORBIDDEN, message) 12 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LocalPointerManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import ru.bozaro.gitlfs.common.Constants 5 | import ru.bozaro.gitlfs.common.data.* 6 | import ru.bozaro.gitlfs.server.ContentManager.HeaderProvider 7 | import java.io.IOException 8 | import java.net.URI 9 | import java.util.* 10 | 11 | /** 12 | * Pointer manager for local ContentManager. 13 | * 14 | * @author Artem V. Navrotskiy 15 | */ 16 | class LocalPointerManager(private val manager: ContentManager, contentLocation: String) : PointerManager { 17 | private val contentLocation: String = if (contentLocation.endsWith("/")) contentLocation else "$contentLocation/" 18 | 19 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 20 | override fun checkUploadAccess(request: HttpServletRequest, selfUrl: URI): PointerManager.Locator { 21 | val headerProvider: HeaderProvider = manager.checkUploadAccess(request) 22 | return createLocator(request, headerProvider, selfUrl) 23 | } 24 | 25 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 26 | override fun checkDownloadAccess(request: HttpServletRequest, selfUrl: URI): PointerManager.Locator { 27 | val headerProvider: HeaderProvider = manager.checkDownloadAccess(request) 28 | return createLocator(request, headerProvider, selfUrl) 29 | } 30 | 31 | private fun createLocator( 32 | request: HttpServletRequest, 33 | headerProvider: HeaderProvider, 34 | selfUrl: URI 35 | ): PointerManager.Locator { 36 | val header = headerProvider.createHeader(createDefaultHeader(request)) 37 | return object : PointerManager.Locator { 38 | @Throws(IOException::class) 39 | override fun getLocations(metas: Array): Array { 40 | return metas.map { getLocation(header, selfUrl, it) }.toTypedArray() 41 | } 42 | 43 | @Throws(IOException::class) 44 | fun getLocation(header: Map, selfUrl: URI, meta: Meta): BatchItem { 45 | val storageMeta = manager.getMetadata(meta.oid) 46 | if (storageMeta != null && meta.size >= 0 && storageMeta.size != meta.size) return BatchItem( 47 | meta, 48 | Error(422, "Invalid object size") 49 | ) 50 | val links: MutableMap = EnumMap(LinkType::class.java) 51 | val link = Link(selfUrl.resolve(contentLocation).resolve(meta.oid), header, null) 52 | if (storageMeta == null) links[LinkType.Upload] = link else links[LinkType.Download] = link 53 | links[LinkType.Verify] = link 54 | return BatchItem(storageMeta ?: meta, links) 55 | } 56 | } 57 | } 58 | 59 | private fun createDefaultHeader(request: HttpServletRequest): Map { 60 | val auth = request.getHeader(Constants.HEADER_AUTHORIZATION) 61 | val header = HashMap() 62 | if (auth != null) { 63 | header[Constants.HEADER_AUTHORIZATION] = auth 64 | } 65 | return header 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LockManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import ru.bozaro.gitlfs.common.LockConflictException 5 | import ru.bozaro.gitlfs.common.VerifyLocksResult 6 | import ru.bozaro.gitlfs.common.data.Lock 7 | import ru.bozaro.gitlfs.common.data.Ref 8 | import java.io.IOException 9 | 10 | interface LockManager { 11 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 12 | fun checkDownloadAccess(request: HttpServletRequest): LockRead 13 | 14 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 15 | fun checkUploadAccess(request: HttpServletRequest): LockWrite 16 | interface LockRead { 17 | @Throws(IOException::class) 18 | fun getLocks(path: String?, lockId: String?, ref: Ref?): List 19 | } 20 | 21 | interface LockWrite : LockRead { 22 | @Throws(LockConflictException::class, IOException::class) 23 | fun lock(path: String, ref: Ref?): Lock 24 | 25 | @Throws(LockConflictException::class, IOException::class) 26 | fun unlock(lockId: String, force: Boolean, ref: Ref?): Lock? 27 | 28 | @Throws(IOException::class) 29 | fun verifyLocks(ref: Ref?): VerifyLocksResult 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/LocksServlet.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.ServletException 4 | import jakarta.servlet.http.HttpServlet 5 | import jakarta.servlet.http.HttpServletRequest 6 | import jakarta.servlet.http.HttpServletResponse 7 | import ru.bozaro.gitlfs.common.JsonHelper 8 | import ru.bozaro.gitlfs.common.LockConflictException 9 | import ru.bozaro.gitlfs.common.data.* 10 | import ru.bozaro.gitlfs.server.LockManager.LockRead 11 | import ru.bozaro.gitlfs.server.LockManager.LockWrite 12 | import ru.bozaro.gitlfs.server.internal.ObjectResponse 13 | import ru.bozaro.gitlfs.server.internal.ResponseWriter 14 | import java.io.IOException 15 | 16 | class LocksServlet(private val lockManager: LockManager) : HttpServlet() { 17 | @Throws(ServletException::class, IOException::class) 18 | override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { 19 | try { 20 | val lockRead = lockManager.checkDownloadAccess(req) 21 | if (req.pathInfo == null) { 22 | listLocks(req, lockRead).write(resp) 23 | return 24 | } 25 | } catch (e: ServerError) { 26 | PointerServlet.sendError(resp, e) 27 | return 28 | } 29 | super.doGet(req, resp) 30 | } 31 | 32 | @Throws(ServletException::class, IOException::class) 33 | override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { 34 | try { 35 | PointerServlet.checkMimeTypes(req) 36 | val lockWrite = lockManager.checkUploadAccess(req) 37 | when { 38 | req.pathInfo == null -> { 39 | createLock(req, lockWrite).write(resp) 40 | return 41 | } 42 | "/verify" == req.pathInfo -> { 43 | verifyLocks(req, lockWrite).write(resp) 44 | return 45 | } 46 | req.pathInfo.endsWith("/unlock") -> { 47 | deleteLock(req, lockWrite, req.pathInfo.substring(1, req.pathInfo.length - 7)).write(resp) 48 | return 49 | } 50 | } 51 | } catch (e: ServerError) { 52 | PointerServlet.sendError(resp, e) 53 | return 54 | } 55 | super.doPost(req, resp) 56 | } 57 | 58 | @Throws(IOException::class) 59 | private fun createLock(req: HttpServletRequest, lockWrite: LockWrite): ResponseWriter { 60 | val createLockReq = JsonHelper.mapper.readValue(req.inputStream, CreateLockReq::class.java) 61 | return try { 62 | val lock = lockWrite.lock(createLockReq.path, createLockReq.ref) 63 | ObjectResponse(HttpServletResponse.SC_CREATED, CreateLockRes(lock)) 64 | } catch (e: LockConflictException) { 65 | ObjectResponse(HttpServletResponse.SC_CONFLICT, LockConflictRes(e.message!!, e.lock)) 66 | } 67 | } 68 | 69 | @Throws(IOException::class) 70 | private fun verifyLocks(req: HttpServletRequest, lockWrite: LockWrite): ResponseWriter { 71 | val verifyLocksReq = JsonHelper.mapper.readValue(req.inputStream, VerifyLocksReq::class.java) 72 | val result = lockWrite.verifyLocks(verifyLocksReq.ref) 73 | return ObjectResponse(HttpServletResponse.SC_OK, VerifyLocksRes(result.ourLocks, result.theirLocks, null)) 74 | } 75 | 76 | @Throws(IOException::class, ServerError::class) 77 | private fun deleteLock( 78 | req: HttpServletRequest, 79 | lockWrite: LockWrite, 80 | lockId: String 81 | ): ResponseWriter { 82 | val deleteLockReq = JsonHelper.mapper.readValue(req.inputStream, DeleteLockReq::class.java) 83 | return try { 84 | val lock = lockWrite.unlock(lockId, deleteLockReq.isForce(), deleteLockReq.ref) 85 | ?: throw ServerError(HttpServletResponse.SC_NOT_FOUND, String.format("Lock %s not found", lockId)) 86 | ObjectResponse(HttpServletResponse.SC_OK, CreateLockRes(lock)) 87 | } catch (e: LockConflictException) { 88 | ObjectResponse(HttpServletResponse.SC_FORBIDDEN, CreateLockRes(e.lock)) 89 | } 90 | } 91 | 92 | @Throws(IOException::class) 93 | private fun listLocks(req: HttpServletRequest, lockRead: LockRead): ResponseWriter { 94 | val refName = req.getParameter("refspec") 95 | val path = req.getParameter("path") 96 | val lockId = req.getParameter("id") 97 | val locks = lockRead.getLocks(path, lockId, Ref.create(refName)) 98 | return ObjectResponse(HttpServletResponse.SC_OK, LocksRes(locks, null)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/PointerManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import ru.bozaro.gitlfs.common.data.BatchItem 5 | import ru.bozaro.gitlfs.common.data.Meta 6 | import java.io.IOException 7 | import java.net.URI 8 | 9 | /** 10 | * Interface for lookup pointer information. 11 | * 12 | * @author Artem V. Navrotskiy 13 | */ 14 | interface PointerManager { 15 | /** 16 | * Check access for upload objects. 17 | * 18 | * @param request HTTP request. 19 | * @param selfUrl Http URL for this request. 20 | * @return Location provider. 21 | */ 22 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 23 | fun checkUploadAccess(request: HttpServletRequest, selfUrl: URI): Locator 24 | 25 | /** 26 | * Check access for download objects. 27 | * 28 | * @param request HTTP request. 29 | * @param selfUrl Http URL for this request. 30 | * @return Location provider. 31 | */ 32 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 33 | fun checkDownloadAccess(request: HttpServletRequest, selfUrl: URI): Locator 34 | 35 | fun interface Locator { 36 | /** 37 | * @param metas Object hash array (note: metadata can have negative size for GET object request). 38 | * @return Return batch items with same order and same count as metas array. 39 | */ 40 | @Throws(IOException::class) 41 | fun getLocations(metas: Array): Array 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/ServerError.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletResponse 4 | 5 | /** 6 | * Server side error exception. 7 | * 8 | * @author Artem V. Navrotskiy 9 | */ 10 | open class ServerError : Exception { 11 | val statusCode: Int 12 | 13 | constructor(statusCode: Int, message: String?) : super(message) { 14 | this.statusCode = statusCode 15 | } 16 | 17 | constructor(statusCode: Int, message: String?, cause: Throwable?) : super(message, cause) { 18 | this.statusCode = statusCode 19 | } 20 | 21 | open fun updateHeaders(response: HttpServletResponse) {} 22 | } 23 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/UnauthorizedError.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.http.HttpServletResponse 4 | 5 | /** 6 | * Unauthorized error. 7 | * 8 | * @author Artem V. Navrotskiy 9 | */ 10 | class UnauthorizedError(private val authenticate: String) : 11 | ServerError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") { 12 | override fun updateHeaders(response: HttpServletResponse) { 13 | super.updateHeaders(response) 14 | response.addHeader("WWW-Authenticate", authenticate) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/internal/ObjectResponse.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server.internal 2 | 3 | import jakarta.servlet.http.HttpServletResponse 4 | import ru.bozaro.gitlfs.common.Constants.MIME_LFS_JSON 5 | import ru.bozaro.gitlfs.common.JsonHelper 6 | import java.io.IOException 7 | 8 | /** 9 | * Response with simple JSON output. 10 | * 11 | * @author Artem V. Navrotskiy 12 | */ 13 | class ObjectResponse(private val status: Int, private val value: Any) : ResponseWriter { 14 | @Throws(IOException::class) 15 | override fun write(response: HttpServletResponse) { 16 | response.status = status 17 | response.contentType = MIME_LFS_JSON 18 | JsonHelper.mapper.writeValue(response.outputStream, value) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /gitlfs-server/src/main/kotlin/ru/bozaro/gitlfs/server/internal/ResponseWriter.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server.internal 2 | 3 | import jakarta.servlet.http.HttpServletResponse 4 | import java.io.IOException 5 | 6 | /** 7 | * HTTP response writer. 8 | * 9 | * @author Artem V. Navrotskiy 10 | */ 11 | fun interface ResponseWriter { 12 | @Throws(IOException::class) 13 | fun write(response: HttpServletResponse) 14 | } 15 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/EmbeddedHttpServer.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import jakarta.servlet.Servlet 4 | import org.eclipse.jetty.server.HttpConnectionFactory 5 | import org.eclipse.jetty.server.Server 6 | import org.eclipse.jetty.server.ServerConnector 7 | import org.eclipse.jetty.servlet.ServletHandler 8 | import org.eclipse.jetty.servlet.ServletHolder 9 | import java.net.URI 10 | import java.net.URISyntaxException 11 | 12 | /** 13 | * Embedded HTTP server for servlet testing. 14 | * 15 | * @author Artem V. Navrotskiy 16 | */ 17 | class EmbeddedHttpServer : AutoCloseable { 18 | private val server: Server = Server() 19 | private val http: ServerConnector = ServerConnector(server, HttpConnectionFactory()) 20 | private val servletHandler: ServletHandler 21 | 22 | val base: URI 23 | get() = try { 24 | URI("http", null, http.host, http.localPort, null, null, null) 25 | } catch (e: URISyntaxException) { 26 | throw IllegalStateException(e) 27 | } 28 | 29 | fun addServlet(pathSpec: String, servlet: Servlet) { 30 | servletHandler.addServletWithMapping(ServletHolder(servlet), pathSpec) 31 | } 32 | 33 | @Throws(Exception::class) 34 | override fun close() { 35 | server.stop() 36 | server.join() 37 | } 38 | 39 | init { 40 | http.port = 0 41 | http.host = "127.0.1.1" 42 | server.addConnector(http) 43 | servletHandler = ServletHandler() 44 | server.handler = servletHandler 45 | server.start() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/EmbeddedLfsServer.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import ru.bozaro.gitlfs.client.auth.AuthProvider 4 | 5 | /** 6 | * Embedded LFS server for servlet testing. 7 | * 8 | * @author Artem V. Navrotskiy 9 | */ 10 | class EmbeddedLfsServer(val storage: MemoryStorage, lockManager: LockManager?) : AutoCloseable { 11 | private val server: EmbeddedHttpServer = EmbeddedHttpServer() 12 | 13 | val authProvider: AuthProvider 14 | get() = storage.getAuthProvider(server.base.resolve("/foo/bar.git/info/lfs")) 15 | 16 | @Throws(Exception::class) 17 | override fun close() { 18 | server.close() 19 | } 20 | 21 | init { 22 | server.addServlet("/foo/bar.git/info/lfs/objects/*", PointerServlet(storage, "/foo/bar.git/info/lfs/storage/")) 23 | server.addServlet("/foo/bar.git/info/lfs/storage/*", ContentServlet(storage)) 24 | if (lockManager != null) server.addServlet("/foo/bar.git/info/lfs/locks/*", LocksServlet(lockManager)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/LocksTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | import ru.bozaro.gitlfs.client.Client 6 | import ru.bozaro.gitlfs.common.LockConflictException 7 | import ru.bozaro.gitlfs.common.data.Ref 8 | 9 | class LocksTest { 10 | @Test 11 | @Throws(Exception::class) 12 | fun simple() { 13 | val storage = MemoryStorage(-1) 14 | EmbeddedLfsServer(storage, MemoryLockManager(storage)).use { server -> 15 | val auth = server.authProvider 16 | val client = Client(auth) 17 | val ref = Ref.create("ref/heads/master") 18 | val lock = client.lock("qwe", ref) 19 | Assert.assertNotNull(lock) 20 | try { 21 | client.lock("qwe", ref) 22 | Assert.fail() 23 | } catch (e: LockConflictException) { 24 | Assert.assertEquals(lock.id, e.lock.id) 25 | } 26 | run { 27 | val locks = client.listLocks("qwe", null, ref) 28 | Assert.assertEquals(locks.size, 1) 29 | Assert.assertEquals(locks[0].id, lock.id) 30 | } 31 | run { 32 | val locks = client.verifyLocks(ref) 33 | Assert.assertEquals(locks.ourLocks.size, 1) 34 | Assert.assertEquals(locks.ourLocks[0].id, lock.id) 35 | Assert.assertEquals(locks.theirLocks.size, 0) 36 | } 37 | val unlock = client.unlock(lock.id, true, ref) 38 | Assert.assertNotNull(unlock) 39 | Assert.assertEquals(unlock!!.id, lock.id) 40 | Assert.assertNull(client.unlock(lock.id, false, ref)) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/MemoryLockManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import com.google.common.base.Strings 4 | import jakarta.servlet.http.HttpServletRequest 5 | import ru.bozaro.gitlfs.common.LockConflictException 6 | import ru.bozaro.gitlfs.common.VerifyLocksResult 7 | import ru.bozaro.gitlfs.common.data.Lock 8 | import ru.bozaro.gitlfs.common.data.Ref 9 | import ru.bozaro.gitlfs.common.data.User 10 | import ru.bozaro.gitlfs.server.LockManager.LockRead 11 | import ru.bozaro.gitlfs.server.LockManager.LockWrite 12 | import java.io.IOException 13 | import java.util.* 14 | import java.util.concurrent.atomic.AtomicInteger 15 | import java.util.stream.Collectors 16 | 17 | class MemoryLockManager(private val contentManager: ContentManager) : LockManager, LockWrite { 18 | private val nextId = AtomicInteger(1) 19 | private val locks = ArrayList() 20 | 21 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 22 | override fun checkDownloadAccess(request: HttpServletRequest): LockRead { 23 | contentManager.checkDownloadAccess(request) 24 | return this 25 | } 26 | 27 | @Throws(IOException::class, ForbiddenError::class, UnauthorizedError::class) 28 | override fun checkUploadAccess(request: HttpServletRequest): LockWrite { 29 | contentManager.checkUploadAccess(request) 30 | return this 31 | } 32 | 33 | override fun getLocks(path: String?, lockId: String?, ref: Ref?): List { 34 | var stream = locks.stream() 35 | if (!Strings.isNullOrEmpty(path)) stream = stream.filter { lock: Lock -> lock.path == path } 36 | if (!Strings.isNullOrEmpty(lockId)) stream = stream.filter { lock: Lock -> lock.id == lockId } 37 | return stream.collect(Collectors.toList()) 38 | } 39 | 40 | @Throws(LockConflictException::class) 41 | override fun lock(path: String, ref: Ref?): Lock { 42 | for (lock in locks) if (lock.path == path) throw LockConflictException(lock) 43 | val lock = Lock(nextId.incrementAndGet().toString(), path, Date(), User("Jane Doe")) 44 | locks.add(lock) 45 | return lock 46 | } 47 | 48 | override fun unlock(lockId: String, force: Boolean, ref: Ref?): Lock? { 49 | var lock: Lock? = null 50 | for (l in locks) { 51 | if (l.id == lockId) { 52 | lock = l 53 | break 54 | } 55 | } 56 | if (lock == null) return null 57 | locks.remove(lock) 58 | return lock 59 | } 60 | 61 | override fun verifyLocks(ref: Ref?): VerifyLocksResult { 62 | return VerifyLocksResult(locks, emptyList()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/MemoryStorage.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import com.google.common.hash.Hashing 5 | import com.google.common.io.ByteStreams 6 | import jakarta.servlet.http.HttpServletRequest 7 | import org.testng.Assert 8 | import ru.bozaro.gitlfs.client.Client.Companion.generateMeta 9 | import ru.bozaro.gitlfs.client.auth.AuthProvider 10 | import ru.bozaro.gitlfs.client.auth.CachedAuthProvider 11 | import ru.bozaro.gitlfs.client.io.StreamProvider 12 | import ru.bozaro.gitlfs.common.Constants 13 | import ru.bozaro.gitlfs.common.data.Link 14 | import ru.bozaro.gitlfs.common.data.Meta 15 | import ru.bozaro.gitlfs.common.data.Operation 16 | import ru.bozaro.gitlfs.server.ContentManager.Downloader 17 | import ru.bozaro.gitlfs.server.ContentManager.Uploader 18 | import java.io.ByteArrayInputStream 19 | import java.io.FileNotFoundException 20 | import java.io.IOException 21 | import java.io.InputStream 22 | import java.net.URI 23 | import java.util.concurrent.ConcurrentHashMap 24 | import java.util.concurrent.atomic.AtomicInteger 25 | 26 | /** 27 | * Simple in-memory storage. 28 | * 29 | * @author Artem V. Navrotskiy 30 | */ 31 | class MemoryStorage(private val tokenMaxUsage: Int) : ContentManager { 32 | private val storage: MutableMap = ConcurrentHashMap() 33 | private val tokenId = AtomicInteger(0) 34 | 35 | @Throws(UnauthorizedError::class) 36 | override fun checkDownloadAccess(request: HttpServletRequest): Downloader { 37 | if (token != request.getHeader(Constants.HEADER_AUTHORIZATION)) { 38 | throw UnauthorizedError("Basic realm=\"Test\"") 39 | } 40 | return object : Downloader { 41 | @Throws(IOException::class) 42 | override fun openObject(hash: String): InputStream { 43 | val data = storage[hash] ?: throw FileNotFoundException() 44 | return ByteArrayInputStream(data) 45 | } 46 | 47 | override fun openObjectGzipped(hash: String): InputStream? { 48 | return null 49 | } 50 | } 51 | } 52 | 53 | private val token: String 54 | get() = if (tokenMaxUsage > 0) { 55 | val token = tokenId.incrementAndGet() 56 | "Bearer Token-" + token / tokenMaxUsage 57 | } else { 58 | "Bearer Token-" + tokenId.get() 59 | } 60 | 61 | @Throws(UnauthorizedError::class) 62 | override fun checkUploadAccess(request: HttpServletRequest): Uploader { 63 | if (token != request.getHeader(Constants.HEADER_AUTHORIZATION)) { 64 | throw UnauthorizedError("Basic realm=\"Test\"") 65 | } 66 | val storage = this 67 | return object : Uploader { 68 | override fun saveObject(meta: Meta, content: InputStream) { 69 | storage.saveObject(meta, content) 70 | } 71 | } 72 | } 73 | 74 | override fun getMetadata(hash: String): Meta? { 75 | val data = storage[hash] 76 | return if (data == null) null else Meta(hash, data.size.toLong()) 77 | } 78 | 79 | @Throws(IOException::class) 80 | fun saveObject(meta: Meta, content: InputStream) { 81 | val data = ByteStreams.toByteArray(content) 82 | if (meta.size >= 0) { 83 | Assert.assertEquals(meta.size, data.size.toLong()) 84 | } 85 | Assert.assertEquals(meta.oid, Hashing.sha256().hashBytes(data).toString()) 86 | storage[meta.oid] = data 87 | } 88 | 89 | @Throws(IOException::class) 90 | fun saveObject(provider: StreamProvider) { 91 | val meta = generateMeta(provider) 92 | provider.stream.use { stream -> saveObject(meta, stream) } 93 | } 94 | 95 | fun getObject(oid: String): ByteArray? { 96 | return storage[oid] 97 | } 98 | 99 | fun getAuthProvider(href: URI): AuthProvider { 100 | return object : CachedAuthProvider() { 101 | override fun getAuthUncached(operation: Operation): Link { 102 | return Link(href, ImmutableMap.of(Constants.HEADER_AUTHORIZATION, token), null) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /gitlfs-server/src/test/kotlin/ru/bozaro/gitlfs/server/ServerTest.kt: -------------------------------------------------------------------------------- 1 | package ru.bozaro.gitlfs.server 2 | 3 | import com.google.common.io.ByteStreams 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import ru.bozaro.gitlfs.client.Client 7 | import ru.bozaro.gitlfs.client.Client.Companion.generateMeta 8 | import ru.bozaro.gitlfs.client.io.StringStreamProvider 9 | import ru.bozaro.gitlfs.common.data.BatchReq 10 | import ru.bozaro.gitlfs.common.data.Operation 11 | import java.io.FileNotFoundException 12 | import java.io.InputStream 13 | 14 | /** 15 | * Git LFS server implementation test. 16 | * 17 | * @author Artem V. Navrotskiy 18 | */ 19 | class ServerTest { 20 | @Test 21 | @Throws(Exception::class) 22 | fun simpleTest() { 23 | EmbeddedLfsServer(MemoryStorage(-1), null).use { server -> 24 | val auth = server.authProvider 25 | val client = Client(auth) 26 | val streamProvider = StringStreamProvider("Hello, world") 27 | val meta = generateMeta(streamProvider) 28 | // Not uploaded yet. 29 | try { 30 | client.getObject(meta.oid) { `in`: InputStream -> ByteStreams.toByteArray(`in`) } 31 | Assert.fail() 32 | } catch (ignored: FileNotFoundException) { 33 | } 34 | client.postBatch(BatchReq(Operation.Download, listOf(meta))) 35 | // Can upload. 36 | Assert.assertTrue(client.putObject(streamProvider, meta)) 37 | // Can download uploaded. 38 | val content = client.getObject(meta.oid) { `in`: InputStream -> ByteStreams.toByteArray(`in`) } 39 | Assert.assertEquals(content, ByteStreams.toByteArray(streamProvider.stream)) 40 | // Already uploaded. 41 | Assert.assertFalse(client.putObject(streamProvider, meta)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-as-svn/git-lfs-java/e262811ab6421aaa4a1f31ba24410ce24a2544f4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | file(".").listFiles().forEach { 2 | if (File(it, "build.gradle.kts").exists()) 3 | include(it.name) 4 | } 5 | --------------------------------------------------------------------------------