├── .gitignore
├── README.md
├── README_en.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
├── resources
│ ├── admin
│ │ ├── admin.css
│ │ ├── admin.html
│ │ ├── admin.js
│ │ ├── bower.json
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── partials
│ │ │ ├── connectors.html
│ │ │ ├── expireRules.html
│ │ │ ├── immediate404Rules.html
│ │ │ ├── login.html
│ │ │ ├── modifyPassword.html
│ │ │ ├── parameters.html
│ │ │ ├── proxies.html
│ │ │ └── upstreams.html
│ │ └── repox.config.json
│ ├── application.conf
│ └── logback.xml
└── scala
│ └── com
│ └── gtan
│ └── repox
│ ├── ExpirationManager.scala
│ ├── ExpirationMigrator.scala
│ ├── FileDeleter.scala
│ ├── GetAsyncHandler.scala
│ ├── GetMaster.scala
│ ├── GetQueueWorker.scala
│ ├── GetWorker.scala
│ ├── Head404Cache.scala
│ ├── HeadAsyncHandler.scala
│ ├── HeadMaster.scala
│ ├── HeadQueueWorker.scala
│ ├── HeadWorker.scala
│ ├── HttpHelpers.scala
│ ├── JsonSerializer.scala
│ ├── Main.scala
│ ├── Repox.scala
│ ├── RequestQueueMaster.scala
│ ├── Requests.scala
│ ├── TimeoutableFuture.scala
│ ├── Timestamp.scala
│ ├── admin
│ ├── AuthHandler.scala
│ ├── ConnectorVO.scala
│ ├── ConnectorsHandler.scala
│ ├── ExpireRulesHandler.scala
│ ├── Immediate404RulesHandler.scala
│ ├── ParametersHandler.scala
│ ├── ProxiesHandler.scala
│ ├── RepoVO.scala
│ ├── ResetHandler.scala
│ ├── RestHandler.scala
│ ├── StaticAssetHandler.scala
│ ├── UpstreamsHandler.scala
│ └── WebConfigHandler.scala
│ ├── config
│ ├── Config.scala
│ ├── ConfigFormats.scala
│ ├── ConfigPersister.scala
│ ├── ConfigQuery.scala
│ ├── ConnectorPersister.scala
│ ├── ExpireRulePersister.scala
│ ├── Immediate404RulePersister.scala
│ ├── ParameterPersister.scala
│ ├── ProxyPersister.scala
│ └── RepoPersister.scala
│ └── data
│ ├── Connector.scala
│ ├── DurationFormat.scala
│ ├── ExpireRule.scala
│ ├── Immediate404Rule.scala
│ ├── ProxyServer.scala
│ └── Repo.scala
└── test
└── scala
└── com
└── gtan
└── repox
├── GetMasterSuite.scala
└── RepoxSuite.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | .gradle/
3 | .idea/
4 | *.iml
5 | *.log
6 | *.swp
7 | .DS_Store
8 |
9 | target/
10 | bower_components/
11 | node_modules/
12 | angular-semantic-ui/
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :exclamation: 第一个 2.12 版发布,assembly 包减小了近 5M(11.8%)
2 |
3 | :exclamation: 2016年5月27日,Repox 公服出口带宽升至2M
4 |
5 | :exclamation: Repox 公服正式迁移至阿里云, 名称由 "广谈公服" 改为 "社区公服". 正在募捐, [查看详情](http://centaur.github.io/repox/)
6 |
7 | [](https://gitter.im/Centaur/repox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8 |
9 | ### Repox是什么
10 | Repox的主要目标是改善sbt解决依赖的速度,但由于它的服务方式与url的格式无关,因此也支持ivy, gradle, maven, leiningen客户端,可以作为nexus/artifactory的替代品来搭建私服。
11 |
12 | 如果对Repox的背景不感兴趣,希望快速脏手,可立即前往[入门指南](https://github.com/Centaur/repox/wiki/入门指南)
13 |
14 | ### 为什么需要Repox
15 | * 每次sbt项目的依赖发生变更,或是sbt版本升级时,sbt会先resolve很长时间,然后download很长时间,甚至有时显示downloading,但你发现事实上没有网络流量产生,sbt假活。
16 | * 为了对已经下载过的依赖进行缓存,保证后续请求能够快速完成,你安装了nexus,并把所有sbt可能用到的仓库以及国内的代理仓库(如oschina等)都设置为nexus的上游,却发现它对于sbt更新依赖慢的问题并没有什么改善。
17 | * 你试图安装typesafe官方使用的Artifactory,却发现其开源版本不支持sbt所使用的自定义url格式。
18 |
19 | ### sbt为什么慢
20 | * sbt默认所使用的仓库都在国外。
21 | * 从某一个版本起,sbt默认使用https协议访问仓库。由于众所周知的原因,连往国外的https连接不稳定,而sbt对这种网络连接的不稳定没有相应的发现和重试机制。
22 | * sbt对依赖的请求分为四个步骤:
23 | 1. resolve : 向所有仓库(内置的如typesafe repo, central maven等,以及build中添加的resolvers)逐个发送HEAD请求询问是否收藏有此文件
24 | 2. download : 向收藏了此文件的仓库发送Get请求下载文件
25 | 3. resolve checksum : 向上游仓库发送HEAD请求询问是否有此文件的sha1 checksum
26 | 4. download checksum : 如果HEAD返回200,则下载 sha1 文件并保证从原文件计算出的sha1与checksum文件的内容一致
27 | * 如果没有组织内私服,那么只在每个开发者的本地`~/.ivy2/cache`目录下有依赖缓存,无法在多个开发者之间共享。
28 | * 如果我们需要源码包(这对使用ide的开发者很正常),sbt连巨大的javadoc包也要一起下载。
29 | * 各种bug,例如 [这个](https://github.com/sbt/sbt/issues/413)
30 |
31 | ### 为什么nexus私服对sbt没什么帮助
32 | * nexus是纯Maven仓库,不代理ivy格式的文件。
33 | * sbt的resolve使用的是HEAD请求,即使是已经缓存过的文件,nexus对HEAD请求每次仍要再到上游仓库去重新逐个询问。
34 | * nexus中的多个上游仓库的次序是设定的。对于尚未缓存的文件,在download环节nexus不会根据HEAD请求的结果剔除掉不包含此文件的仓库。
35 | * 根据我们的试用,nexus开源版本质量不高,在不佳的网络条件下,其应用内部状态很容易就僵死。
36 |
37 | ### Repox的理念
38 | * 代理所有流量
39 |
40 | 这样可以在所有环节进行优化。
41 |
42 | * 要么快速完成,要么快速失败
43 |
44 | * 如果上游仓库中有请求的文件,尽量选取最快的仓库下载。
45 | * 下载过程中如果发现当前连往上游仓库的网络连接长久没有获得数据,则终止重试。
46 | * 如果所有上游仓库都失败,则向sbt返回404,让用户重试,而不是永久等待。
47 |
48 | **重要提示:这一条意味着使用repox时,sbt将有更大的概率更新失败,提示“download failed”错误。这时只需要重试失败的指令即可。我们认为多重试几次是比漫长的等待更友好的用户体验。**
49 |
50 | * 与url的格式无关
51 |
52 | 因此除了改善sbt解决依赖的速度,Repox 还可以为ivy, gradle, maven, leiningen客户端服务。
53 |
54 | * 做代理该做的事
55 |
56 | 对于已经下载过的文件,保证resolve立即成功。
57 |
58 | ### Repox的策略
59 | * 对已下载的文件
60 |
61 | 无论是HEAD还是GET请求都立即响应。
62 |
63 | * 对首次请求的文件
64 |
65 | * HEAD请求:同时向所有上游仓库发出询问,取最早200响应的头信息返回给sbt
66 | * GET请求:
67 | * 首先根据HEAD请求的结果对上游仓库进行过滤,HEAD返回非200的仓库被排除
68 | * 上游仓库被指定不同的优先级,向同一优先级的所有仓库同时发送GET请求,选取第一个返回200的仓库下载,其它仓库的连接被终止
69 | * 当某一优先级的所有仓库均失败(返回非200或超时)时,自动进入下一优先级
70 | * 如果所有仓库都失败,则404
71 | * 支持断点续传
72 | * 有些资源只在某特定的仓库中收藏,而到这个仓库的网络状况比较差。Repox可为某个上游仓库指定HTTP代理
73 | * 可设置全面禁止javadoc包的下载
74 |
75 | 更多的细节请参阅其它Wiki页。
76 |
77 | ### 社区公服 404 了怎么办
78 | 现在越来越多的 artifact 没有被放进 maven 中央仓库, 而是零散地托管在一些小众仓库中。如果社区公服中没有将这样的小众仓库添加为上游仓库, 会导致404.
79 | 这时候你可以临时将 `sbt.override.build.repos` 设置为 `false`, 待依赖解析下载完成后恢复成 `true`; 或者选择一种方式申请向社区公服添加上游仓库:
80 |
81 | * 到 github 上提交 issue
82 | * 发邮件给 43284683 AT qq.com
83 |
84 | ### Repox的优势
85 | * 轻
86 |
87 | 核心代码仅十几个文件,每个文件不超过200行。约一个周末的阅读量。
88 |
89 | * 全异步非阻塞
90 |
91 | undertow + akka + async-http-client
92 |
93 | * 提供了web配置界面来根据用户的网络状况对仓库、代理、参数等进行微调
94 |
95 | * 实用
96 |
97 | Repox是由我们团队自己的需求驱动的,它支持着我们的日常开发任务。
98 |
99 | ### 推荐的使用方式
100 | 1. 作为组织内私服运行
101 |
102 | 这是最推荐的使用方式,这样组织内有一个开发者完成一个项目的依赖更新后,其它开发者能够飞速更新。
103 |
104 | 1. 开发者本机运行
105 |
106 | repox的部署和运行都非常简单,在本机运行也能显著提高生产力。
107 |
--------------------------------------------------------------------------------
/README_en.md:
--------------------------------------------------------------------------------
1 | [](https://gitter.im/Centaur/repox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2 |
3 | ### What is Repox
4 | The main motivation of Repox is to speedup sbt dependency resolving. But it can be used as ivy/gradle/ maven/leiningen proxy as well,as a replacement of Nexus/Artifactory.
5 |
6 | Currently Repox only do proxying, there is no support for artifact publishing.
7 |
8 | To get your hands dirty immediately, go [Getting Start](https://github.com/Centaur/repox/wiki/Getting-Started)
9 |
10 | ### Why Repox
11 |
12 | We have tried Nexus and Artifactory open source version as private proxy, neither has brought improvements for sbt projects.
13 |
14 | ### Repox Philosophy
15 |
16 | * Proxy Everything
17 |
18 | So that we can introduce improvments at every step.
19 |
20 | * Succeed fast or Fail fast
21 |
22 | * If multiple upstream repos have the request file, choose the fastest one.
23 | * If a connection did not get data for a long time, abort and redownload。
24 | * Remember failed request to make less duplicate.
25 |
26 | **Important Note: This means that repox may have more chance to “download failed”。You just reinvoke the failed command. We believe that more retry brings better experience than waiting forever. **
27 |
28 | * Do what a proxy is supposed to do
29 |
30 | If a file has ever been downloaded, response immediately, without asking upstreams.
31 |
32 | Read the wiki pages for details.
33 |
34 | ### Repox Pros
35 |
36 | * Lightweight
37 |
38 | Just a dozen of source code files for the core functionality, each less then 200 lines. Just a weekend's reading.
39 |
40 | * Reactive overall
41 |
42 | undertow + akka + async-http-client
43 |
44 | * Adjust configurations by web admin
45 |
46 | * Practical
47 |
48 | We build Repox for our own needs and we are using it everyday.
49 |
50 | ### Suggested Deployment
51 | 1. As Organization Private Proxy
52 |
53 | Recommended. So that once a developer has done `sbt update` a project, others' updating feels like using local repo.
54 |
55 | 1. Run locally
56 |
57 | The deployment and running are very simple,it brings good productivity too when running locally。
58 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "repox"
2 |
3 | organization := "com.gtan"
4 |
5 | scalaVersion := "2.12.6"
6 |
7 | val akkaVersion = "2.5.14"
8 |
9 | libraryDependencies ++= {
10 | val undertowVer = "2.0.11.Final"
11 | val logbackVer = "1.2.3"
12 | val leveldbVer = "0.7"
13 | val leveldbjniVer = "1.8"
14 | val scalaTestVer = "3.0.5"
15 | val playJsonVer = "2.6.7"
16 | val scalaLoggingVer = "3.9.0"
17 | val ningVer = "1.9.40"
18 | val protobufVer = "3.6.1"
19 | Seq(
20 | "io.undertow" % "undertow-core" % undertowVer,
21 | ("com.ning" % "async-http-client" % ningVer)
22 | .exclude("org.slf4j", "slf4j-api"),
23 | ("com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVer)
24 | .exclude("org.scala-lang", "scala-library")
25 | .exclude("org.scala-lang", "scala-reflect"),
26 | ("ch.qos.logback" % "logback-classic" % logbackVer)
27 | .exclude("org.slf4j", "slf4j-api"),
28 | ("com.typesafe.akka" %% "akka-actor" % akkaVersion)
29 | .exclude("org.scala-lang", "scala-library")
30 | .exclude("org.slf4j", "slf4j-api"),
31 | ("com.typesafe.akka" %% "akka-slf4j" % akkaVersion)
32 | .exclude("org.scala-lang", "scala-library")
33 | .exclude("org.slf4j", "slf4j-api"),
34 | ("com.typesafe.akka" %% "akka-agent" % akkaVersion)
35 | .exclude("org.scala-lang", "scala-library"),
36 | ("com.typesafe.akka" %% "akka-persistence" % akkaVersion)
37 | .exclude("org.scala-lang", "scala-library"),
38 | ("org.iq80.leveldb" % "leveldb" % leveldbVer)
39 | .exclude("com.google.guava", "guava"),
40 | "org.fusesource.leveldbjni" % "leveldbjni-all" % leveldbjniVer,
41 | ("com.typesafe.akka" %% "akka-persistence-query" % akkaVersion)
42 | .exclude("org.scala-lang", "scala-library")
43 | .exclude("com.typesafe", "config")
44 | .exclude("com.typesafe", "ssl-config-akka_2.12")
45 | .exclude("com.typesafe.akka", "akka-testkit_2.12")
46 | .exclude("com.typesafe.akka", "akka-stream-testkit_2.12"),
47 | ("com.typesafe.play" %% "play-json" % playJsonVer)
48 | .exclude("org.scala-lang", "scala-library"),
49 | "com.google.protobuf" % "protobuf-java" % protobufVer,
50 | "org.scalatest" %% "scalatest" % scalaTestVer % "test"
51 | )
52 | }
53 |
54 | transitiveClassifiers := Seq("sources")
55 | updateOptions := updateOptions.value.withGigahorse(false)
56 | scalacOptions ++= Seq(
57 | "-feature",
58 | "-deprecation",
59 | "-language:implicitConversions",
60 | // "-language:higherKinds",
61 | // "-language:existentials",
62 | "-language:postfixOps"
63 | )
64 |
65 | fork := true
66 |
67 | assemblyMergeStrategy in assembly := {
68 | case str@PathList("admin", "bower_components", remains@_*) => remains match {
69 | case Seq("angular", "angular.min.js") => MergeStrategy.deduplicate
70 | case Seq("angular-route", "angular-route.min.js") => MergeStrategy.deduplicate
71 | case Seq("ng-file-upload", "ng-file-upload.min.js") => MergeStrategy.deduplicate
72 | case Seq("underscore", "underscore-min.js") => MergeStrategy.deduplicate
73 | case Seq("jquery", "dist", "jquery.min.js") => MergeStrategy.deduplicate
74 | case Seq("semantic-ui", "dist", "semantic.min.css") => MergeStrategy.deduplicate
75 | case Seq("semantic-ui", "dist", "semantic.min.js") => MergeStrategy.deduplicate
76 | case Seq("semantic-ui", "dist", "themes", "default", "assets", _*) => MergeStrategy.deduplicate
77 | case _ => MergeStrategy.discard
78 | }
79 | case x =>
80 | (assemblyMergeStrategy in assembly).value.apply(x)
81 | }
82 |
83 | mainClass in assembly := Some("com.gtan.repox.Main")
84 |
85 | test in assembly := {}
86 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.0
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 |
2 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.7")
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/main/resources/admin/admin.css:
--------------------------------------------------------------------------------
1 | #page-header {
2 | margin-top: 0.5em ;
3 | margin-left: 0.5em ;
4 | }
5 |
6 | .ui.table thead th, .ui.table td {
7 | text-align: center;
8 | }
9 |
10 | .ui.tiny.icon.button {
11 | padding: .39em;
12 | }
13 |
14 | .ui.modal {
15 | margin-top: -15%;
16 | }
17 | .ui.modal > .header {
18 | padding: 0.6rem 2rem;
19 | }
20 |
21 | .ui.form select {
22 | height: 2.5em;
23 | line-height: 2.5em;
24 | }
25 |
26 | .ui.authForm {
27 | width: 50%;
28 | margin: 0 auto;
29 | }
30 |
31 | #progressBar {
32 | position: fixed;
33 | top: 0;
34 | left: 0;
35 | height: 2px;
36 | background-color: seagreen;
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/resources/admin/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/main/resources/admin/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "repox",
3 | "version": "0.0.0",
4 | "homepage": "https://github.com/Centaur/repox",
5 | "authors": [
6 | "oldpig <43284683@qq.com>"
7 | ],
8 | "keywords": [
9 | "repox"
10 | ],
11 | "license": "MIT",
12 | "private": true,
13 | "ignore": [
14 | "**/.*",
15 | "node_modules",
16 | "bower_components",
17 | "test",
18 | "tests"
19 | ],
20 | "dependencies": {
21 | "semantic-ui": "1.12.2",
22 | "angular": "~1.3.6",
23 | "angular-route": "~1.3.6",
24 | "underscore": "*",
25 | "ng-file-upload": "~10.1.9"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/resources/admin/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Centaur/repox/714a17dcdcbae13f59963874412a17818525d3e8/src/main/resources/admin/favicon.ico
--------------------------------------------------------------------------------
/src/main/resources/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 | 如果浏览器没有自动跳转, 请点击 这里
11 |
12 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/connectors.html:
--------------------------------------------------------------------------------
1 |
47 |
48 |
50 |
51 |
52 |
55 |
117 |
118 |
119 |
121 |
122 |
123 |
126 |
188 |
189 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/expireRules.html:
--------------------------------------------------------------------------------
1 |
36 |
37 |
59 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/immediate404Rules.html:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
44 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
82 |
83 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/login.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/modifyPassword.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/parameters.html:
--------------------------------------------------------------------------------
1 |
23 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/proxies.html:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
48 |
72 |
73 |
74 |
75 |
76 |
77 |
80 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/main/resources/admin/partials/upstreams.html:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
66 |
67 |
70 |
133 |
134 |
135 |
137 |
138 |
139 |
142 |
205 |
206 |
--------------------------------------------------------------------------------
/src/main/resources/admin/repox.config.json:
--------------------------------------------------------------------------------
1 | {"proxies":[{"id":1,"name":"Lantern","protocol":"HTTP","host":"localhost","port":8787,"disabled":false},{"id":2,"name":"Vultr","protocol":"HTTP","host":"108.61.163.157","port":8888,"disabled":false}],"repos":[{"id":1,"name":"koala","base":"http://nexus.openkoala.org/content/groups/Koala-release","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":7},{"id":4,"name":"oschina","base":"http://maven.oschina.net/content/groups/public","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":7},{"id":3,"name":"typesafe","base":"http://dl.bintray.com/typesafe/ivy-releases","priority":2,"getOnly":false,"maven":false,"disabled":false},{"id":10,"name":"jcenter","base":"http://jcenter.bintray.com","priority":2,"getOnly":false,"maven":true,"disabled":false},{"id":16,"name":"typesafe-maven","base":"http://dl.bintray.com/typesafe/maven-releases/","priority":2,"getOnly":false,"maven":true,"disabled":false},{"id":5,"name":"sbt-plugin","base":"http://dl.bintray.com/sbt/sbt-plugin-releases","priority":4,"getOnly":false,"maven":false,"disabled":false},{"id":9,"name":"scalajs","base":"http://dl.bintray.com/content/scala-js/scala-js-releases","priority":4,"getOnly":false,"maven":false,"disabled":false},{"id":7,"name":"central","base":"http://repo1.maven.org/maven2","priority":4,"getOnly":false,"maven":true,"disabled":false},{"id":14,"name":"tut-plugin","base":"http://dl.bintray.com/content/tpolecat/sbt-plugin-releases","priority":5,"getOnly":false,"maven":false,"disabled":false},{"id":6,"name":"scalaz","base":"http://dl.bintray.com/scalaz/releases","priority":5,"getOnly":false,"maven":true,"disabled":false},{"id":11,"name":"spray","base":"http://repo.spray.io","priority":5,"getOnly":false,"maven":true,"disabled":false},{"id":2,"name":"sonatype","base":"https://oss.sonatype.org/content/repositories/releases","priority":5,"getOnly":false,"maven":true,"disabled":false},{"id":15,"name":"tut-bintray","base":"http://dl.bintray.com/tpolecat/maven","priority":5,"getOnly":false,"maven":true,"disabled":false},{"id":17,"name":"twitter","base":"https://maven.twttr.com","priority":5,"getOnly":false,"maven":true,"disabled":true},{"id":8,"name":"ibiblio","base":"http://mirrors.ibiblio.org/maven2","priority":6,"getOnly":false,"maven":true,"disabled":true},{"id":12,"name":"scoverage","base":"http://dl.bintray.com/sksamuel/sbt-plugins","priority":5,"getOnly":false,"maven":false,"disabled":false},{"id":13,"name":"gatling-plugin","base":"http://dl.bintray.com/content/gatling/sbt-plugins","priority":5,"getOnly":false,"maven":false,"disabled":false}],"connectorUsage":[{"repo":{"id":1,"name":"koala","base":"http://nexus.openkoala.org/nexus/content/groups/Koala-release","priority":1,"getOnly":true,"maven":true,"disabled":false},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":9,"name":"scalajs","base":"http://dl.bintray.com/content/scala-js/scala-js-releases","priority":4,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":5,"name":"sbt-plugin","base":"http://dl.bintray.com/sbt/sbt-plugin-releases","priority":4,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":1,"name":"koala","base":"http://nexus.openkoala.org/content/groups/Koala-release","priority":1,"getOnly":true,"maven":true,"disabled":false},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":16,"name":"typesafe-maven","base":"https://repo.typesafe.com/typesafe/maven-releases/","priority":2,"getOnly":false,"maven":true,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":16,"name":"typesafe-maven","base":"http://dl.bintray.com/typesafe/maven-releases/","priority":2,"getOnly":false,"maven":true,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":1,"name":"koala","base":"http://nexus.openkoala.org/content/groups/Koala-release","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":7},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":4,"name":"oschina","base":"http://maven.oschina.net/content/groups/public","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":8},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":10,"name":"jcenter","base":"http://jcenter.bintray.com","priority":2,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":2,"name":"sonatype","base":"http://oss.sonatype.org/content/repositories/releases","priority":5,"getOnly":false,"maven":true,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":2,"name":"sonatype","base":"https://oss.sonatype.org/content/repositories/releases","priority":5,"getOnly":false,"maven":true,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":4,"name":"oschina","base":"http://maven.oschina.net/content/groups/public","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":7},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":2,"name":"sonatype","base":"http://oss.sonatype.org/content/repositories/releases","priority":5,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":3,"name":"typesafe","base":"http://dl.bintray.com/typesafe/ivy-releases","priority":2,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":3,"name":"typesafe","base":"http://repo.typesafe.com/typesafe/releases","priority":2,"getOnly":false,"maven":false,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":1,"name":"koala","base":"http://nexus.openkoala.org/content/groups/Koala-release","priority":1,"getOnly":true,"maven":true,"disabled":false,"parentId":8},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":4,"name":"oschina","base":"http://maven.oschina.net/content/groups/public","priority":1,"getOnly":true,"maven":true,"disabled":false},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":4,"name":"oschina","base":"http://maven.oschina.net/content/groups/public","priority":2,"getOnly":true,"maven":true,"disabled":false},"connector":{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}},{"repo":{"id":10,"name":"jcenter","base":"http://jcenter.bintray.com","priority":2,"getOnly":false,"maven":true,"disabled":false},"connector":{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000}}],"proxyUsage":[{"connector":{"id":4,"name":"use-proxy","connectionTimeout":"6 seconds","connectionIdleTimeout":"20 seconds","maxConnections":50,"maxConnectionsPerHost":25},"proxy":{"id":2,"name":"Vultr","protocol":"HTTP","host":"108.61.163.157","port":8888,"disabled":false}}],"immediate404Rules":[{"id":1,"include":".+-javadoc\\.jar","exclude":"/net/sourceforge/f2j/arpack_combined_all/.*-javadoc.jar","disabled":false},{"id":2,"include":".+-parent.*\\.jar","disabled":false},{"id":3,"include":"(/.+)+/((.+?-project)(_(.+?)(_(.+))?)?)/(.+?)/\\3-\\8(-(.+?))?\\.jar","exclude":"/org/apache/maven/maven-project/(.+?)/maven-project-\\1\\.jar","disabled":false},{"id":4,"include":"(/.+)+/((.+?-pom)(_(.+?)(_(.+))?)?)/(.+?)/\\3-\\8(-(.+?))?\\.jar","disabled":false},{"id":5,"include":"/.+?/(.+?-project)/.+/\\1\\.jar","disabled":false},{"id":6,"include":"/org/jboss/xnio/xnio-all/.+\\.jar","disabled":false},{"id":7,"include":"/org\\.jboss\\.xnio/xnio-all/.+\\.jar","disabled":false},{"id":8,"include":"/org/apache/apache/(\\d+)/.+\\.jar","disabled":false},{"id":9,"include":"/org\\.apache/apache/(\\d+)/.+\\.jar","disabled":false},{"id":10,"include":"/com/google/google/(\\d+)/.+\\.jar","disabled":false},{"id":11,"include":"/com\\.google/google/(\\d+)/.+\\.jar","disabled":false},{"id":12,"include":"/org/ow2/ow2/.+\\.jar","disabled":false},{"id":13,"include":"/org\\.ow2/ow2/.+\\.jar","disabled":false},{"id":14,"include":"(/.+)+/((.+?-site)(_(.+?)(_(.+))?)?)/(.+?)/\\3-\\8(-(.+?))?\\.jar","exclude":"","disabled":false},{"id":15,"include":"/.+?/(.+?-site)/.+/\\1\\.jar","exclude":"/com\\.typesafe\\.sbt/sbt-site/.*","disabled":false},{"id":16,"include":"/org/fusesource/leveldbjni/.+-sources\\.jar","disabled":false},{"id":17,"include":"/org\\.fusesource\\.leveldbjni/.+-sources\\.jar","disabled":false},{"id":18,"include":"/.+?/(.+?-pom)/.+/\\1\\.jar","disabled":false},{"id":19,"include":"/org/webjars/.+-sources.jar","disabled":false},{"id":20,"include":"/org\\.webjars/.+-sources.jar","disabled":false},{"id":21,"include":"/org/scala-sbt/(.+?)/(.+?)/\\1-\\2\\..*","exclude":"/org/scala-sbt/(launcher|test)-interface/(.+?)/\\1-interface-\\2.*","disabled":true}],"expireRules":[{"id":1,"pattern":".+/maven-metadata\\.xml","duration":"1 day","disabled":false}],"connectors":[{"id":1,"name":"default","connectionTimeout":"5 seconds","connectionIdleTimeout":"10 seconds","maxConnections":3000,"maxConnectionsPerHost":1000},{"id":2,"name":"fast-upstream","connectionTimeout":"5 seconds","connectionIdleTimeout":"5 seconds","maxConnections":3000,"maxConnectionsPerHost":1000},{"id":3,"name":"slow-upstream","connectionTimeout":"15 seconds","connectionIdleTimeout":"30 seconds","maxConnections":3000,"maxConnectionsPerHost":1000},{"id":4,"name":"use-proxy","connectionTimeout":"6 seconds","connectionIdleTimeout":"20 seconds","maxConnections":50,"maxConnectionsPerHost":25}],"headTimeout":"10 seconds","headRetryTimes":3,"password":"not exported","extraResources":["/home/xiefei/.m2/repository"]}
--------------------------------------------------------------------------------
/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | loggers = ["akka.event.slf4j.Slf4jLogger"]
3 | loglevel = debug
4 | jvm-exit-on-fatal-error = off
5 | actor {
6 | debug {
7 | lifecycle = off
8 | receive = off
9 | unhandled = off
10 | router-misconfiguration = off
11 | }
12 | serializers {
13 | jsonSerializer = "com.gtan.repox.JsonSerializer"
14 | }
15 | serialization-bindings {
16 | "java.io.Serializable" = none
17 | "com.gtan.repox.config.Jsonable" = jsonSerializer
18 | "com.gtan.repox.config.Evt" = jsonSerializer
19 | }
20 | # default-dispatcher {
21 | # #executor = "thread-pool-executor"
22 | # #thread-pool-executor {
23 | # # task-queue-type = "a"
24 | # #}
25 | # fork-join-executor {
26 | # parallelism-min = 2
27 | # parallelism-factor = 8.0
28 | # parallelism-max = 8
29 | # }
30 | # throughput = 20
31 | # }
32 | }
33 | persistence {
34 | journal {
35 | plugin = "akka.persistence.journal.leveldb"
36 | leveldb.dir = ${?HOME}${?HOMEPATH}"/.repox/journal"
37 | }
38 | snapshot-store {
39 | plugin = "akka.persistence.snapshot-store.local"
40 | local.dir = ${?HOME}${?HOMEPATH}"/.repox/snapshots"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | %d{MM-dd HH:mm:ss.SSS} %highlight(%-5level) %logger{36} %X{akkaSource} %n %blue(%msg) %n %n
8 |
9 |
10 |
11 |
12 |
13 |
14 | ${user.home}/.repox/repox.log
15 |
16 | ${user.home}/.repox/repox.log.%i
17 | 1
18 | 9
19 |
20 |
21 | 100MB
22 |
23 |
25 |
26 | %d{MM-dd HH:mm:ss.SSS} %-5level %logger{36} %X{akkaSource} %n %msg %n %n
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/ExpirationManager.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.time.temporal.ChronoUnit
4 | import java.time.{LocalDateTime, ZoneId}
5 |
6 | import akka.actor.{ActorLogging, Cancellable, Props}
7 | import akka.persistence._
8 | import com.gtan.repox.config.{Evt, Jsonable}
9 | import play.api.libs.json.{JsValue, Json}
10 |
11 | import scala.concurrent.duration._
12 | import scala.language.postfixOps
13 |
14 |
15 | object ExpirationManager extends SerializationSupport {
16 |
17 | case class CreateExpiration(uri: String, duration: Duration)
18 |
19 | case class CancelExpiration(uri: String)
20 |
21 | case class PerformExpiration(uri: String)
22 |
23 | case class ExpirationPerformed(uri: String) extends Jsonable with Evt
24 |
25 | case class Expiration(uri: String, timestamp: LocalDateTime) extends Jsonable with Evt
26 |
27 | case class ExpirationSeq(expirations: Seq[Expiration]) extends Jsonable with Evt
28 |
29 | implicit val expirationFormat = Json.format[Expiration]
30 | implicit val expirationPerformedFormat = Json.format[ExpirationPerformed]
31 | implicit val expirationSeqFormat = Json.format[ExpirationSeq]
32 |
33 | val ExpirationClass = classOf[Expiration].getName
34 | val ExpirationPerformedClass = classOf[ExpirationPerformed].getName
35 | val ExpirationSeqClass = classOf[ExpirationSeq].getName
36 |
37 | override val reader: JsValue => PartialFunction[String, Jsonable] = payload => {
38 | case ExpirationClass => payload.as[Expiration]
39 | case ExpirationPerformedClass => payload.as[ExpirationPerformed]
40 | case ExpirationSeqClass => payload.as[ExpirationSeq]
41 | }
42 |
43 | override val writer: PartialFunction[Jsonable, JsValue] = {
44 | case o: Expiration => Json.toJson(o)
45 | case o: ExpirationPerformed => Json.toJson(o)
46 | case o: ExpirationSeq => Json.toJson(o)
47 | }
48 | }
49 |
50 |
51 | /**
52 | * This actor always recover the latest snapshot so that performed or canceled expirations will not be seen in the future.
53 | * To exam the detailed history, use another persistence query to recovery all events.
54 | */
55 | class ExpirationManager extends PersistentActor with ActorLogging {
56 |
57 | import com.gtan.repox.ExpirationManager._
58 |
59 | import scala.concurrent.ExecutionContext.Implicits.global
60 |
61 | override def persistenceId: String = "Expiration"
62 |
63 | // The following 2 states are kept in-sync during each repox process.
64 | // memory only
65 | var scheduledExpirations: Map[Expiration, Cancellable] = Map.empty
66 | // persistent
67 | var unperformed: ExpirationSeq = ExpirationSeq(Vector.empty)
68 |
69 | def scheduleFileDelete(expiration: Expiration): Unit = {
70 | if (expiration.timestamp.isAfter(LocalDateTime.now)) {
71 | val delay: Long = expiration.timestamp.atZone(ZoneId.systemDefault).toInstant.toEpochMilli -
72 | LocalDateTime.now.atZone(ZoneId.systemDefault).toInstant.toEpochMilli
73 | log.debug(s"Schedule expiration for ${expiration.uri} at ${expiration.timestamp} in $delay ms")
74 | val cancellable = Repox.system.scheduler.scheduleOnce(
75 | delay.millis,
76 | self,
77 | PerformExpiration(expiration.uri))
78 | scheduledExpirations = scheduledExpirations.updated(expiration, cancellable)
79 | } else {
80 | log.debug(s"${expiration.uri} expired, trigger FileDelete now.")
81 | context.actorOf(Props(classOf[FileDeleter], expiration.uri, 'ExpirationPersister))
82 | }
83 | }
84 |
85 | def cancelExpirations(pattern: String): Unit = {
86 | val canceled = scheduledExpirations.collect {
87 | case (expiration, cancellable) if expiration.uri.matches(pattern) =>
88 | cancellable.cancel()
89 | expiration
90 | }.toSet
91 | scheduledExpirations = scheduledExpirations.filterKeys(canceled.contains)
92 | unperformed = unperformed.copy(expirations = unperformed.expirations.filterNot(_.uri.matches(pattern)))
93 | }
94 |
95 | override def receiveRecover: Receive = {
96 | case e@Expiration(uri, timestamp) =>
97 | unperformed = unperformed.copy(expirations = unperformed.expirations :+ e)
98 | scheduleFileDelete(e)
99 | case CancelExpiration(pattern) =>
100 | cancelExpirations(pattern)
101 | case SnapshotOffer(metadata, saved) =>
102 | this.unperformed = saved.asInstanceOf[ExpirationSeq]
103 | for(expiration <- unperformed.expirations) {
104 | scheduleFileDelete(expiration)
105 | }
106 | }
107 |
108 | override def receiveCommand: Receive = {
109 | case CreateExpiration(uri, duration) =>
110 | if (!scheduledExpirations.exists(_._1.uri == uri)) {
111 | val timestamp = LocalDateTime.now().plus(duration.toMillis.toInt, ChronoUnit.MILLIS)
112 | val expiration = Expiration(uri, timestamp)
113 | persist(expiration) { _ => }
114 | unperformed = unperformed.copy(expirations = unperformed.expirations :+ expiration)
115 | scheduleFileDelete(expiration)
116 | } else {
117 | // this can only happen when there were multiple request in lined in GetQueueWorker stash queue
118 | }
119 | case CancelExpiration(pattern) =>
120 | cancelExpirations(pattern)
121 | persist(CancelExpiration(pattern)) { _ =>
122 | saveSnapshot(unperformed)
123 | }
124 | case PerformExpiration(uri) =>
125 | log.debug(s"$uri expired, trigger FileDelete now.")
126 | context.actorOf(Props(classOf[FileDeleter], uri, 'ExpirationPersister))
127 | case e@ExpirationPerformed(uri) =>
128 | scheduledExpirations = scheduledExpirations.filterKeys(_.uri != uri)
129 | unperformed.expirations.find(_.uri == uri).foreach { performed =>
130 | unperformed = unperformed.copy(expirations = unperformed.expirations.filterNot(_ == performed))
131 | persist(performed) { _ =>
132 | saveSnapshot(unperformed)
133 | }
134 | }
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/ExpirationMigrator.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.nio.file.Paths
4 |
5 | import akka.actor.{Props, ActorSystem, ActorLogging}
6 | import akka.persistence.{DeleteMessagesSuccess, RecoveryCompleted, PersistentActor}
7 | import com.gtan.repox.ExpirationManager.Expiration
8 |
9 | import scala.util.Properties._
10 |
11 | class ExpirationMigrator extends PersistentActor {
12 |
13 | val storagePath = Paths.get(userHome, ".repox", "storage")
14 |
15 | def resolveToPaths(uri: String) = (storagePath.resolve(uri.tail), storagePath.resolve(uri.tail + ".sha1"))
16 |
17 | println("Start migration ...")
18 | override def receiveRecover: Receive = {
19 | case e@Expiration(uri, timestamp) =>
20 | val (path, sha1Path) = resolveToPaths(uri)
21 | path.toFile.delete()
22 | sha1Path.toFile.delete()
23 | println(s"$path and $sha1Path deleted.")
24 | case RecoveryCompleted => // This is the last message that receiveRecover would receive
25 | deleteMessages(Long.MaxValue)
26 | }
27 |
28 | override def receiveCommand: Receive = {
29 | case DeleteMessagesSuccess(_) =>
30 | println("Migration finished.")
31 | context.system.terminate()
32 | }
33 |
34 | override def persistenceId: String = "Expiration"
35 |
36 | }
37 |
38 | object ExpirationMigrator {
39 | def main(args: Array[String]) {
40 | val system = ActorSystem("ExpirationMigrator")
41 | val migrator = system.actorOf(Props[ExpirationMigrator], "Migrator")
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/FileDeleter.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import akka.actor.{PoisonPill, Actor, ActorLogging}
4 | import com.gtan.repox.ExpirationManager.ExpirationPerformed
5 |
6 | object FileDeleter {
7 | case object Quarantined
8 | }
9 |
10 | /**
11 | * delete file and sha1 file as a whole
12 | * @param uri
13 | */
14 | class FileDeleter(uri: String, initiator: Symbol) extends Actor with ActorLogging {
15 | import FileDeleter._
16 |
17 | log.debug(s"$initiator initiated deletion of $uri")
18 | Repox.requestQueueMaster ! RequestQueueMaster.Quarantine(uri: String)
19 | override def receive = {
20 | case Quarantined =>
21 | val (path, sha1Path) = Repox.resolveToPaths(uri)
22 | path.toFile.delete()
23 | sha1Path.toFile.delete()
24 | Repox.requestQueueMaster ! RequestQueueMaster.FileDeleted(uri)
25 | log.debug(s"$path and $sha1Path deleted.")
26 | if (initiator == 'ExpirationPersister) {
27 | context.parent ! ExpirationPerformed(uri)
28 | }
29 | self ! PoisonPill
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/GetAsyncHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.io.{File, FileOutputStream}
4 | import java.nio.channels.FileChannel
5 | import java.nio.file.{Files, Paths}
6 | import java.security.SecureRandom
7 |
8 | import akka.actor.{ActorRef, PoisonPill}
9 | import com.gtan.repox.GetWorker._
10 | import com.gtan.repox.Head404Cache.NotFound
11 | import com.gtan.repox.config.Config
12 | import com.gtan.repox.data.Repo
13 | import com.ning.http.client.AsyncHandler.STATE
14 | import com.ning.http.client.{AsyncHandler, HttpResponseBodyPart, HttpResponseHeaders, HttpResponseStatus}
15 | import com.typesafe.scalalogging.LazyLogging
16 | import io.undertow.util.StatusCodes
17 |
18 | class GetAsyncHandler(val uri: String,
19 | val repo: Repo,
20 | val worker: ActorRef, // associated GetWorker
21 | val master: ActorRef, /* associated GetWorker's GetMaster */
22 | val tempFilePath: Option[String]) extends AsyncHandler[Unit] with LazyLogging {
23 |
24 | private var tempFileOs: FileOutputStream = null
25 | var tempFile: File = null
26 | private var contentType: String = null
27 |
28 | @volatile private var canceled = false
29 |
30 | def cancel(deleteTempFile: Boolean = true) {
31 | canceled = true
32 | cleanup(deleteTempFile)
33 | }
34 |
35 | override def onThrowable(t: Throwable): Unit = {
36 | if (!canceled)
37 | worker ! AsyncHandlerThrows(t)
38 | }
39 |
40 | override def onCompleted(): Unit = {
41 | if (!canceled) {
42 | if (tempFileOs != null) {
43 | logger.debug(s"onCompleted: tempFile.length = ${tempFile.length()}")
44 | tempFileOs.close()
45 | }
46 | if (tempFile != null) {
47 | // completed before parent notify PeerChosen or self cancel
48 | master.!(Completed(tempFile.toPath, repo))(worker)
49 | }
50 | }
51 | }
52 |
53 | override def onBodyPartReceived(bodyPart: HttpResponseBodyPart): STATE = {
54 | if (!canceled) {
55 | bodyPart.writeTo(tempFileOs)
56 | worker ! PartialDataReceived(bodyPart.length())
57 | STATE.CONTINUE
58 | } else {
59 | cleanup()
60 | STATE.ABORT
61 | }
62 | }
63 |
64 | override def onStatusReceived(responseStatus: HttpResponseStatus): STATE = {
65 | val statusCode: Int = responseStatus.getStatusCode
66 | if (statusCode == StatusCodes.NOT_FOUND) {
67 | Repox.head404Cache ! NotFound(uri, repo)
68 | }
69 | if (canceled) {
70 | cleanup()
71 | STATE.ABORT
72 | } else {
73 | if (statusCode < 200 || statusCode >= 400) {
74 | logger.debug(s"Get ${repo.absolute(uri)} $statusCode")
75 | master.!(UnsuccessResponseStatus(responseStatus))(worker)
76 | cleanup()
77 | STATE.ABORT
78 | } else
79 | STATE.CONTINUE
80 | }
81 | }
82 |
83 | override def onHeadersReceived(headers: HttpResponseHeaders): STATE = {
84 | logger.debug(s"${repo.absolute(uri)} headers ================== \n ${headers.getHeaders}")
85 | if (!canceled) {
86 | val newContentType = headers.getHeaders.getFirstValue("Content-type")
87 | if (contentType == null || contentType == newContentType) {
88 | if (contentType == null) contentType = newContentType
89 | if (tempFile != null) {
90 | logger.debug("Lantern interrupted. Resync data.")
91 | tempFileOs.close()
92 | tempFileOs = new FileOutputStream(tempFile)
93 | worker ! HeadersGot(headers)
94 | } else {
95 | tempFile = newOrReuseTempFile()
96 | tempFileOs = new FileOutputStream(tempFile, true)
97 | worker ! HeadersGot(headers)
98 | master.!(HeadersGot(headers))(worker)
99 | }
100 | STATE.CONTINUE
101 | } else {
102 | worker ! LanternGiveup
103 | cleanup()
104 | STATE.ABORT
105 | }
106 | } else {
107 | cleanup()
108 | STATE.ABORT
109 | }
110 | }
111 |
112 | def cleanup(deleteTempFile: Boolean = true): Unit = {
113 | if (tempFileOs != null) {
114 | tempFileOs.close()
115 | }
116 | if (tempFile != null && deleteTempFile) {
117 | tempFile.delete()
118 | }
119 | }
120 |
121 | private def newOrReuseTempFile(): File = tempFilePath match {
122 | case None =>
123 | val tempRoot = Config.storagePath.resolve("temp")
124 | val slot = GetAsyncHandler.secureRandom.nextInt(Int.MaxValue).formatted("%04d").take(4)
125 | val distributedPath = tempRoot.resolve(slot)
126 | Files.createDirectories(distributedPath)
127 | Files.createTempFile(distributedPath, "repox", ".tmp").toFile
128 | case Some(file) =>
129 | new File(file)
130 | }
131 |
132 | }
133 |
134 | object GetAsyncHandler {
135 | val secureRandom = new SecureRandom()
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/GetMaster.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.io.FileInputStream
4 | import java.net.URLEncoder
5 | import java.nio.file.StandardCopyOption._
6 | import java.nio.file.{Files, Path}
7 | import java.security.MessageDigest
8 |
9 | import akka.actor._
10 | import com.gtan.repox.GetWorker.{Cleanup, PeerChosen}
11 | import com.gtan.repox.Head404Cache.{NotFound, Query}
12 | import com.gtan.repox.data.Repo
13 | import com.typesafe.scalalogging.LazyLogging
14 |
15 | import scala.language.postfixOps
16 |
17 | object GetMaster extends LazyLogging {
18 |
19 | import scala.concurrent.duration._
20 |
21 | implicit val timeout = new akka.util.Timeout(1 seconds)
22 |
23 | def fileSha1Hex(path: Path): String = {
24 | val sha1 = MessageDigest.getInstance("SHA-1")
25 | val fis = new FileInputStream(path.toFile)
26 | val buffer = new Array[Byte](8192)
27 | var len = fis.read(buffer)
28 | while (len != -1) {
29 | sha1.update(buffer, 0, len)
30 | len = fis.read(buffer)
31 | }
32 | val ba: Array[Byte] = sha1.digest
33 | ba.map(b => "%02x".format(b)).mkString("")
34 | }
35 | }
36 |
37 | /**
38 | * 负责某一个 uri 的 Get, 可能由多个 upstream Repo 生成多个 GetWorker
39 | *
40 | * @param uri 要获取的 uri
41 | * @param from 可能的 Repo
42 | */
43 | class GetMaster(val uri: String, val from: Seq[Repo]) extends Actor with ActorLogging {
44 |
45 | import scala.concurrent.duration._
46 |
47 | override val supervisorStrategy =
48 | OneForOneStrategy(maxNrOfRetries = 3, withinTimeRange = 1 minute)(super.supervisorStrategy.decider)
49 |
50 | private[this] def excludeNotFound[T](xss: Seq[Seq[T]], notFoundIn: Set[T]): Seq[Seq[T]] =
51 | xss.map(_.filterNot(notFoundIn)).filter(_.nonEmpty)
52 |
53 | val (resolvedPath, resolvedChecksumPath) = Repox.resolveToPaths(uri)
54 | var downloadedTempFilePath: Path = _
55 |
56 | var workerChosen = false
57 | var chosenWorker: ActorRef = _
58 | var chosenRepo: Repo = _
59 | var childFailCount = 0
60 | var candidateRepos = Repox.orderByPriority(
61 | if (Repox.isIvyUri(uri))
62 | from.filterNot(_.maven)
63 | else from
64 | )
65 |
66 | var children: Seq[ActorRef] = _
67 |
68 | private[this] def waitFor404Cache: Receive = {
69 | case Head404Cache.ExcludeRepos(repos) =>
70 | candidateRepos = excludeNotFound(candidateRepos, repos)
71 | if (candidateRepos.isEmpty) {
72 | context.parent ! GetQueueWorker.Get404(uri)
73 | self ! PoisonPill
74 | } else {
75 | log.debug(s"Try ${candidateRepos.map(_.map(_.name))} $uri")
76 | val thisLevel = candidateRepos.head
77 | children = for (upstream <- thisLevel) yield {
78 | startAWorker(upstream, uri)
79 | }
80 | context become gettingFile
81 | }
82 | }
83 |
84 | private[this] def startAWorker(upstream: Repo, target: String): ActorRef = {
85 | context.actorOf(
86 | Props(classOf[GetWorker], upstream, target, None, -1L),
87 | name = s"GetWorker_${URLEncoder.encode(upstream.name, "UTF-8")}_${Repox.nextId}"
88 | )
89 | }
90 |
91 | private[this] def askHead404Cache(): Unit = {
92 | Repox.head404Cache ! Query(uri)
93 | }
94 |
95 | askHead404Cache()
96 |
97 | def receive = waitFor404Cache
98 |
99 | def gettingFile: Receive = {
100 | case GetWorker.Resume(repo, tempFilePath, totalLength) =>
101 | chosenWorker = context.actorOf(
102 | Props(classOf[GetWorker], repo, uri, Some(tempFilePath), totalLength),
103 | name = s"GetWorker_${repo.name}_${Repox.nextId}"
104 | )
105 | children = chosenWorker :: Nil
106 | case GetWorker.Failed(t) =>
107 | if (chosenWorker == sender()) {
108 | log.debug(s"Chosen worker dead. Rechoose")
109 | reset()
110 | } else {
111 | childFailCount += 1
112 | if (childFailCount == children.length) {
113 | candidateRepos.toList match {
114 | case Nil =>
115 | log.debug(s"GetMaster all child failed. 404")
116 | context.parent ! GetQueueWorker.Get404(uri)
117 | self ! PoisonPill
118 | case head :: tail =>
119 | if (from.length > 1) {
120 | log.debug(s"all child failed. to next level.")
121 | candidateRepos = tail
122 | } else {
123 | log.debug(s"only one repo, no other choose, retry. $uri")
124 | }
125 | reset()
126 | }
127 | }
128 | }
129 | case GetWorker.UnsuccessResponseStatus(status) =>
130 | childFailCount += 1
131 | if (childFailCount == children.length) {
132 | candidateRepos.toList match {
133 | case Nil =>
134 | log.info(s"GetMaster all child failed. 404")
135 | context.parent ! GetQueueWorker.Get404(uri)
136 | self ! PoisonPill
137 | case head :: tail =>
138 | if (from.length > 1) {
139 | log.debug(s"all child failed. to next level.")
140 | candidateRepos = tail
141 | } else {
142 | log.debug(s"only one repo, no other choose, retry. $uri")
143 | }
144 | reset()
145 | }
146 | }
147 | case GetWorker.Completed(path, repo) =>
148 | if (sender() == chosenWorker) {
149 | for (child <- children) {
150 | child ! PoisonPill
151 | }
152 | if (uri.endsWith(".sha1")) {
153 | log.debug(s"A standalone .sha1 request, stop trying to retrieve its checksum. $uri")
154 | resolvedPath.getParent.toFile.mkdirs()
155 | java.nio.file.Files.move(path, resolvedPath, REPLACE_EXISTING, ATOMIC_MOVE)
156 | context.parent ! GetQueueWorker.Completed(path, repo, checksumSuccess = true)
157 | self ! PoisonPill
158 | } else {
159 | downloadedTempFilePath = path
160 | chosenRepo = repo
161 | chosenWorker = startAWorker(repo, uri + ".sha1")
162 | children = chosenWorker :: Nil
163 | context become gettingChecksum
164 | }
165 | } else {
166 | sender ! Cleanup
167 | }
168 | case GetWorker.HeadersGot(headers) =>
169 | if (children.contains(sender())) {
170 | if (!workerChosen) {
171 | log.debug(s"chose ${sender().path.name}, canceling others. ")
172 | for (others <- children.filterNot(_ == sender())) {
173 | others ! PeerChosen(sender())
174 | }
175 | chosenWorker = sender()
176 | workerChosen = true
177 | } else if (sender != chosenWorker) {
178 | sender ! PeerChosen(chosenWorker)
179 | }
180 | } else {
181 | log.debug(s"HeadersGot msg from previous level ${sender().path.name} received. Ignore.")
182 | }
183 | }
184 |
185 | def gettingChecksum: Receive = {
186 | case GetWorker.Completed(path, repo) =>
187 | val computed = GetMaster.fileSha1Hex(downloadedTempFilePath)
188 | val downloaded = scala.io.Source.fromFile(path.toFile, "UTF-8").mkString.trim
189 | val checksumSuccess: Boolean = computed.equalsIgnoreCase(downloaded)
190 | if (checksumSuccess || candidateRepos.flatten.size == 1) {
191 | Files.createDirectories(resolvedPath.getParent)
192 | log.info(s"GetWorker ${sender().path.name} completed $uri. Checksum ${if (checksumSuccess) "success" else "failed"}")
193 | Files.move(downloadedTempFilePath, resolvedPath, REPLACE_EXISTING, ATOMIC_MOVE)
194 | Files.move(path, resolvedChecksumPath, REPLACE_EXISTING, ATOMIC_MOVE)
195 | context.parent ! GetQueueWorker.Completed(path, repo, checksumSuccess)
196 | children.foreach(child => child ! Cleanup)
197 | self ! PoisonPill
198 | } else {
199 | log.info(s"Checksum failed for $uri. Try other upstreams.")
200 | Repox.head404Cache ! NotFound(uri, repo)
201 | Repox.head404Cache ! NotFound(uri + ".sha1", repo)
202 | Files.deleteIfExists(downloadedTempFilePath)
203 | Files.deleteIfExists(path)
204 | reset()
205 | }
206 | case GetWorker.Failed(t) =>
207 | log.debug(s"GetWorker failed in gettingChecksum state: ${t.getMessage}. Restart.")
208 | sender ! PoisonPill
209 | chosenWorker = startAWorker(chosenRepo, uri + ".sha1")
210 | children = chosenWorker :: Nil
211 | case GetWorker.UnsuccessResponseStatus(status) =>
212 | if (status.getStatusCode != 404) {
213 | log.debug(s"GetWorker get UnsuccessResponseStatus in gettingChecksum state: $status. Restart.")
214 | sender ! PoisonPill
215 | chosenWorker = startAWorker(chosenRepo, uri + ".sha1")
216 | children = chosenWorker :: Nil
217 | } else {
218 | log.debug(s"Upstream has artifact but not checksum. Response 404 but save downloaded artifact. May generate on repox manually in the future.")
219 | java.nio.file.Files.move(downloadedTempFilePath, resolvedPath, REPLACE_EXISTING, ATOMIC_MOVE)
220 | context.parent ! GetQueueWorker.Get404(uri)
221 | self ! PoisonPill
222 | }
223 | case msg =>
224 | log.debug(s"Received message in gettingChecksum state: $msg. Ignore.")
225 | }
226 |
227 |
228 | private[this] def reset() = {
229 | childFailCount = 0
230 | chosenWorker = null
231 | workerChosen = false
232 | for (child <- children) child ! PoisonPill
233 | askHead404Cache()
234 | if (downloadedTempFilePath != null)
235 | Files.deleteIfExists(downloadedTempFilePath)
236 | context become waitFor404Cache
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/GetQueueWorker.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.nio.file.Path
4 |
5 | import akka.actor._
6 | import com.gtan.repox.ExpirationManager.CreateExpiration
7 | import com.gtan.repox.RequestQueueMaster.KillMe
8 | import com.gtan.repox.config.Config
9 | import com.gtan.repox.data.Repo
10 | import io.undertow.server.HttpServerExchange
11 |
12 | import scala.concurrent.duration._
13 | import scala.language.postfixOps
14 | import scala.util.Random
15 | import scala.util.Random
16 |
17 | object GetQueueWorker {
18 |
19 | case class Get404(uri: String)
20 |
21 | case class Completed(path: Path, repo: Repo, checksumSuccess: Boolean)
22 |
23 | }
24 |
25 | class GetQueueWorker(val uri: String) extends Actor with Stash with ActorLogging {
26 |
27 | import GetQueueWorker._
28 |
29 | override def receive = start
30 |
31 | def start: Receive = {
32 | case msg@Requests.Get(exchange) =>
33 | Repox.downloaded(uri) match {
34 | case Some(Tuple2(resourceManager, resourceHandler)) =>
35 | log.info(s"$uri downloaded. Serve immediately.")
36 | Repox.immediateFile(resourceHandler, exchange)
37 | suicide()
38 | case None =>
39 | log.info(s"$uri not downloaded. Downloading.")
40 | context.actorOf(Props(classOf[GetMaster], uri, Config.enabledRepos), s"GetMaster_${Repox.nextId}")
41 | self ! msg
42 | context become working
43 | }
44 | }
45 |
46 | var found = false
47 |
48 | var deleteFileAfterResponse = false
49 |
50 | def working: Receive = {
51 | case Requests.Get(_) =>
52 | stash()
53 | case result@Completed(path, repo, checksumSuccess) =>
54 | log.debug(s"GetQueueWorker completed $uri")
55 | found = true
56 | deleteFileAfterResponse = !checksumSuccess
57 | unstashAll()
58 | context.setReceiveTimeout(1 second)
59 | context become flushWaiting
60 | case Get404(u) =>
61 | log.debug(s"GetQueueWorker 404 $u")
62 | found = false
63 | unstashAll()
64 | context.setReceiveTimeout(1 second)
65 | context become flushWaiting
66 | }
67 |
68 | def flushWaiting: Receive = {
69 | case Requests.Get(exchange) =>
70 | if (found) {
71 | log.debug(s"flushWaiting $exchange 200. Sending file $uri")
72 | Repox.sendFile(Repox.resourceHandlers.get().apply(Repox.storageManager), exchange)
73 | if (deleteFileAfterResponse) {
74 | context.actorOf(Props(classOf[FileDeleter], uri, 'GetQueueWorker))
75 | } else Config.enabledExpireRules.find(rule => uri.matches(rule.pattern)).foreach { r =>
76 | Repox.expirationPersister ! CreateExpiration(uri, r.duration)
77 | }
78 | } else {
79 | log.debug(s"flushWaiting $exchange 404")
80 | Repox.respond404(exchange)
81 | }
82 | case ReceiveTimeout =>
83 | suicide()
84 | }
85 |
86 | private def suicide(): Unit = {
87 | context.parent ! KillMe(Queue('get, uri))
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/GetWorker.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.io.File
4 | import java.nio.file.Path
5 |
6 | import akka.actor._
7 | import com.gtan.repox.data.Repo
8 | import com.ning.http.client._
9 |
10 | import scala.language.postfixOps
11 |
12 |
13 | object GetWorker {
14 |
15 | case class UnsuccessResponseStatus(responseStatus: HttpResponseStatus)
16 |
17 | case class Failed(t: Throwable)
18 |
19 | case class Resume(upstream: Repo, tempFilePath: String, totalLength: Long)
20 |
21 | case class BodyPartGot(bodyPart: HttpResponseBodyPart)
22 |
23 | case class HeadersGot(headers: HttpResponseHeaders)
24 |
25 | case class Completed(path: Path, repo: Repo)
26 |
27 | case class PeerChosen(who: ActorRef)
28 |
29 | case object Cleanup
30 |
31 | // same effect as ReceiveTimeout
32 | case object LanternGiveup
33 |
34 | case class PartialDataReceived(length: Int)
35 |
36 | case class AsyncHandlerThrows(t: Throwable)
37 |
38 | }
39 |
40 | /**
41 | * 负责某一个 uri 在 某一个 upstream Repo 中的获取
42 | * @param upstream 上游 Repo
43 | * @param uri 要获取的uri
44 | * @param tempFilePath 已下载但未完成的临时文件路径,tempFilePath.isDefined时为续传,否则为新的下载任务
45 | * @param totalLength 文件的真实长度, 仅当tempFilePath.isDefined时使用
46 | */
47 | class GetWorker(val upstream: Repo,
48 | val uri: String,
49 | val tempFilePath: Option[String],
50 | val totalLength: Long) extends Actor with Stash with ActorLogging {
51 |
52 | import com.gtan.repox.GetWorker._
53 | import scala.concurrent.duration._
54 | import scala.collection.JavaConverters._
55 |
56 | val handler = new GetAsyncHandler(uri, upstream, context.self, context.parent, tempFilePath)
57 | var downloaded = 0L
58 | var percentage = 0.0
59 | var contentLength = -1L
60 | var acceptByteRange = false
61 |
62 | val (connector, client) = Repox.clientOf(upstream)
63 |
64 | val requestHeaders = tempFilePath match {
65 | case None => new FluentCaseInsensitiveStringsMap()
66 | .add("Host", List(upstream.host).asJava)
67 | .add("Accept-Encoding", List("identity").asJava) // 禁止压缩, jar文件没有压缩的必要, 其它文件太小不值得.
68 | case Some(file) =>
69 | downloaded = new File(file).length()
70 | contentLength = totalLength
71 | new FluentCaseInsensitiveStringsMap()
72 | .add("Host", List(upstream.host).asJava)
73 | .add("Accept-Encoding", List("identity").asJava) // 禁止压缩
74 | .add("Range", List(s"bytes=$downloaded-").asJava)
75 | }
76 |
77 | val future = client.prepareGet(upstream.absolute(uri))
78 | .setHeaders(requestHeaders)
79 | .execute(handler)
80 |
81 | override def receive = {
82 | case AsyncHandlerThrows(t) =>
83 | log.debug(s"AsyncHandler throws -- ${t.getStackTrace.mkString("\n")}")
84 | tempFilePath match {
85 | case None =>
86 | context.parent ! Failed(t)
87 | case Some(path) =>
88 | log.debug("In a resuming, retry...")
89 | handler.cancel(deleteTempFile = false)
90 | context.parent ! Resume(upstream, path, totalLength)
91 | }
92 | self ! PoisonPill
93 |
94 | case Cleanup =>
95 | handler.cancel()
96 | self ! PoisonPill
97 |
98 | case PeerChosen(who) =>
99 | handler.cancel()
100 | self ! PoisonPill
101 |
102 | case ReceiveTimeout | LanternGiveup =>
103 | log.debug("GetWorker timeout (or lantern giveup).")
104 | handler.cancel(deleteTempFile = false)
105 | self ! PoisonPill
106 | if (acceptByteRange || tempFilePath.isDefined) {
107 | context.parent ! Resume(upstream,
108 | tempFilePath.fold(handler.tempFile.getAbsolutePath)(identity),
109 | tempFilePath.fold(contentLength)(_ => totalLength))
110 | } else {
111 | context.parent ! Failed(new RuntimeException("Chosen worker timeout or lantern giveup"))
112 | }
113 |
114 | case PartialDataReceived(length) =>
115 | downloaded += length
116 | if (contentLength != -1) {
117 | val newPercentage = downloaded * 100.0 / contentLength
118 | if (newPercentage - percentage > 10.0 || downloaded == contentLength) {
119 | log.debug(f"downloaded $downloaded%s bytes. $newPercentage%.2f %%")
120 | percentage = newPercentage
121 | }
122 | } else {
123 | log.debug("PartialDataReceived")
124 | }
125 |
126 | case HeadersGot(headers) =>
127 | tempFilePath match {
128 | case None =>
129 | val contentLengthHeader = headers.getHeaders.getFirstValue("Content-Length")
130 | val acceptRanges = headers.getHeaders.getFirstValue("Accept-Ranges")
131 | if (contentLengthHeader != null) {
132 | log.debug(s"contentLength=$contentLengthHeader")
133 | contentLength = contentLengthHeader.toLong
134 | }
135 | if (acceptRanges != null) {
136 | log.debug(s"accept-ranges=$acceptRanges")
137 | acceptByteRange = acceptRanges.toLowerCase == "bytes"
138 | }
139 | downloaded = 0
140 | percentage = 0.0
141 | case Some(path) =>
142 | downloaded = new File(path).length()
143 | percentage = downloaded * 100.0 / totalLength
144 | log.debug(f"Resuming $uri%s from ${upstream.name}%s, $percentage%.2f %% already downloaded.")
145 | }
146 | context.setReceiveTimeout(connector.connectionIdleTimeout - 1.second)
147 | }
148 |
149 |
150 | }
151 |
152 |
153 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/Head404Cache.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import akka.actor.{Actor, ActorLogging}
4 | import com.gtan.repox.Head404Cache.{NotFound, Query}
5 | import com.gtan.repox.data.Repo
6 |
7 | import scala.language.postfixOps
8 |
9 | /**
10 | * repo that does not have some uri according to previous HEAD request
11 | * hold for 1 day
12 | * User: xf
13 | * Date: 14/11/24
14 | * Time: 下午10:55
15 | */
16 | object Head404Cache {
17 |
18 | case class Query(uri: String) // answer Set[Repo]
19 |
20 | case class NotFound(uri: String, repo: Repo)
21 |
22 | case class ExcludeRepos(repos: Set[Repo])
23 |
24 | import scala.concurrent.duration._
25 |
26 | val idleTimeout = 1 days
27 | }
28 |
29 | class Head404Cache extends Actor with ActorLogging {
30 | import Head404Cache._
31 |
32 | var data = Map.empty[String, Map[Repo, Timestamp]]
33 |
34 | def expired(timestamp: Timestamp): Boolean =
35 | (timestamp + Head404Cache.idleTimeout).isPast
36 |
37 | override def receive = {
38 | case Query(uri) =>
39 | data.get(uri) match {
40 | case None => sender ! ExcludeRepos(Set.empty[Repo])
41 | case Some(map) =>
42 | val notFoundAndNotExpired = map.filter { case (_, timestamp) => !expired(timestamp)}
43 | if(notFoundAndNotExpired.isEmpty){
44 | data = data - uri
45 | } else {
46 | data = data.updated(uri, notFoundAndNotExpired)
47 | }
48 | sender ! ExcludeRepos(notFoundAndNotExpired.keySet)
49 | }
50 | case NotFound(uri, repo) =>
51 | data.get(uri) match {
52 | case None =>
53 | data = data.updated(uri, Map(repo -> Timestamp.now))
54 | case Some(map) =>
55 | val notFoundAndNotExpired = map.filter { case (_, timestamp) => !expired(timestamp)}
56 | data = data.updated(uri, notFoundAndNotExpired + (repo -> Timestamp.now))
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/HeadAsyncHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import akka.actor.ActorRef
4 | import com.gtan.repox.config.Config
5 | import com.gtan.repox.data.Repo
6 | import com.ning.http.client.AsyncHandler.STATE
7 | import com.ning.http.client.{AsyncHandler, HttpResponseBodyPart, HttpResponseHeaders, HttpResponseStatus}
8 | import com.typesafe.scalalogging.LazyLogging
9 | import io.undertow.util.StatusCodes
10 |
11 | class HeadAsyncHandler(val worker: ActorRef,val uri: String, val repo: Repo) extends AsyncHandler[Unit] with LazyLogging {
12 | var statusCode = 200
13 |
14 | @volatile private var canceled = false
15 |
16 | def cancel(): Unit = {
17 | canceled = true
18 | }
19 |
20 | override def onThrowable(t: Throwable): Unit = {
21 | if(!canceled) {
22 | logger.debug("HeadAsyncHandler throws ", t)
23 | worker ! HeadWorker.Failed(t)
24 | }
25 | }
26 |
27 | override def onCompleted(): Unit = {
28 | /* we don't get here actually */
29 | }
30 |
31 | override def onBodyPartReceived(bodyPart: HttpResponseBodyPart): STATE = {
32 | // we don't get here actually
33 | STATE.ABORT
34 | }
35 |
36 | override def onStatusReceived(responseStatus: HttpResponseStatus): STATE = {
37 | if (!canceled) {
38 | statusCode = responseStatus.getStatusCode
39 | statusCode match {
40 | case StatusCodes.NOT_FOUND =>
41 | Repox.head404Cache ! Head404Cache.NotFound(uri, repo)
42 | for(child <- Config.repos.filter(_.parentId.exists(repo.id.contains))){
43 | Repox.head404Cache ! Head404Cache.NotFound(uri, child)
44 | }
45 | case _ =>
46 | }
47 | STATE.CONTINUE
48 | } else STATE.ABORT
49 | }
50 |
51 | override def onHeadersReceived(headers: HttpResponseHeaders): STATE = {
52 | if (!canceled) {
53 | import scala.collection.JavaConverters._
54 | worker ! HeadWorker.Responded(statusCode, mapAsScalaMapConverter(headers.getHeaders).asScala.toMap)
55 | }
56 | STATE.ABORT
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/HeadMaster.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.net.URLEncoder
4 |
5 | import akka.actor.Actor.Receive
6 | import akka.actor._
7 | import com.gtan.repox.Head404Cache.Query
8 | import com.gtan.repox.Repox.ResponseHeaders
9 | import com.gtan.repox.config.Config
10 | import com.gtan.repox.data.Repo
11 | import com.ning.http.client.FluentCaseInsensitiveStringsMap
12 | import io.undertow.server.HttpServerExchange
13 | import io.undertow.util.{Headers, StatusCodes}
14 | import collection.JavaConverters._
15 | import scala.collection.Set
16 | import scala.util.Random
17 |
18 | object HeadMaster {
19 |
20 | case class FoundIn(repo: Repo, headers: ResponseHeaders)
21 |
22 | case class NotFound(repo: Repo)
23 |
24 | case class HeadTimeout(repo: Repo)
25 |
26 | }
27 |
28 | class HeadMaster(val exchange: HttpServerExchange) extends Actor with ActorLogging {
29 |
30 | import com.gtan.repox.HeadMaster._
31 |
32 | val uri = exchange.getRequestURI
33 | val upstreams = {
34 | val candidates = Config.enabledRepos.filterNot(_.getOnly)
35 | if(Repox.isIvyUri(uri))
36 | candidates.filterNot(_.maven)
37 | else candidates
38 | }
39 |
40 | val requestHeaders = new FluentCaseInsensitiveStringsMap()
41 | for (name <- exchange.getRequestHeaders.getHeaderNames.asScala) {
42 | requestHeaders.add(name.toString, exchange.getRequestHeaders.get(name))
43 | }
44 | requestHeaders.put(Headers.ACCEPT_ENCODING_STRING, List("identity").asJava)
45 |
46 |
47 | var children: Seq[ActorRef] = _
48 |
49 | var finishedChildren = 0
50 | var retryTimes = 0
51 |
52 | var candidateRepos: Seq[Repo] = _
53 |
54 | Repox.head404Cache ! Query(uri)
55 |
56 | def start: Receive = {
57 | case Head404Cache.ExcludeRepos(repos) =>
58 | candidateRepos = upstreams.filterNot(repos.contains)
59 | log.debug(s"CandidateRepos: ${candidateRepos.map(_.name)}")
60 | if (candidateRepos.isEmpty) {
61 | context.parent ! HeadQueueWorker.NotFound(exchange)
62 | self ! PoisonPill
63 | }
64 | children = for (upstream <- candidateRepos) yield {
65 | val childName = s"HeadWorker_${URLEncoder.encode(upstream.name, "UTF-8")}_${Repox.nextId}"
66 | context.actorOf(
67 | Props(classOf[HeadWorker], upstream, uri, requestHeaders),
68 | name = childName)
69 | }
70 | context become working
71 | }
72 |
73 | override def receive = start
74 |
75 | def working: Receive = {
76 | case msg@FoundIn(repo, headers) =>
77 | finishedChildren += 1
78 | context.parent ! HeadQueueWorker.FoundIn(repo, headers, exchange)
79 | self ! PoisonPill
80 | case msg@NotFound(repo) =>
81 | finishedChildren += 1
82 | testAllReturned()
83 | case msg@HeadTimeout(repo) =>
84 | finishedChildren += 1
85 | testAllReturned()
86 | }
87 |
88 | def testAllReturned(): Unit = {
89 | if (finishedChildren == children.size) {
90 | // all returned
91 | retryTimes += 1
92 | if (retryTimes == Config.headRetryTimes) {
93 | context.parent ! HeadQueueWorker.NotFound(exchange)
94 | log.debug(s"retried ${Config.headRetryTimes} times, give up.")
95 | self ! PoisonPill
96 | } else {
97 | log.debug("All headworkers return 404. retry.")
98 | finishedChildren = 0
99 | Repox.head404Cache ! Query(uri)
100 | context become start
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/HeadQueueWorker.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import akka.actor._
4 | import com.gtan.repox.RequestQueueMaster.KillMe
5 | import com.gtan.repox.config.Config
6 | import com.gtan.repox.data.Repo
7 | import io.undertow.server.HttpServerExchange
8 | import org.w3c.dom.html.HTMLScriptElement
9 |
10 | import scala.concurrent.duration._
11 | import scala.language.postfixOps
12 | import scala.util.Random
13 |
14 | object HeadQueueWorker {
15 |
16 | case class NotFound(exchange: HttpServerExchange)
17 |
18 | case class FoundIn(repo: Repo, headers: Repox.ResponseHeaders, exchange: HttpServerExchange)
19 |
20 | }
21 |
22 | class HeadQueueWorker(val uri: String) extends Actor with Stash with ActorLogging {
23 |
24 | import HeadQueueWorker._
25 |
26 | override def receive = start
27 |
28 | var found = false
29 | var resultHeaders: Repox.ResponseHeaders = _
30 |
31 | def start: Receive = {
32 | case Requests.Head(exchange) =>
33 | log.debug(s"Recevied Head request of ${exchange.getRequestURI} in START state")
34 | assert(exchange.getRequestURI == uri)
35 | Repox.downloaded(uri) match {
36 | case Some(Tuple2(resourceManager, _)) =>
37 | Repox.immediateHead(resourceManager, exchange)
38 | suicide()
39 | case None =>
40 | for (peers <- Repox.peer(uri)) {
41 | peers.find(p => Repox.downloaded(p).isDefined) match {
42 | case Some(peer) =>
43 | Repox.smart404(exchange)
44 | suicide()
45 | case _ =>
46 | context.actorOf(Props(classOf[HeadMaster], exchange), name = s"HeadMaster_${Repox.nextId}")
47 | context become working
48 | }
49 | }
50 | }
51 | }
52 |
53 | def working: Receive = {
54 | case Requests.Head(exchange) =>
55 | log.debug(s"Recevied Head request of ${exchange.getRequestURI} in WORKING state")
56 | assert(exchange.getRequestURI == uri)
57 | stash()
58 | case result@FoundIn(repo, headers, exchange) =>
59 | found = true
60 | resultHeaders = headers
61 | Repox.respondHead(exchange, headers)
62 | log.info(s"Request HEAD for $uri respond 200.")
63 | unstashAll()
64 | context.setReceiveTimeout(1 second)
65 | context become flushWaiting
66 | case result@NotFound(exchange) =>
67 | found = false
68 | Repox.respond404(exchange)
69 | log.info(s"Tried ${Config.headRetryTimes} times. Give up. Respond with 404. $uri")
70 | unstashAll()
71 | context.setReceiveTimeout(1 second)
72 | context become flushWaiting
73 | }
74 |
75 | def flushWaiting: Receive = {
76 | case Requests.Head(exchange) =>
77 | log.debug(s"Recevied Head request of ${exchange.getRequestURI} in FLUSHWAITING state")
78 | if (found) {
79 | log.debug(s"Send found head: $resultHeaders")
80 | Repox.respondHead(exchange, resultHeaders)
81 | } else {
82 | log.debug(s"Send 404")
83 | Repox.respond404(exchange)
84 | }
85 | case ReceiveTimeout =>
86 | suicide()
87 | }
88 |
89 | def suicide(): Unit = {
90 | context.parent ! KillMe(Queue('head, uri))
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/HeadWorker.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import akka.actor._
4 | import com.gtan.repox.config.Config
5 | import com.gtan.repox.data.Repo
6 | import com.ning.http.client.FluentCaseInsensitiveStringsMap
7 | import io.undertow.util.{Headers, StatusCodes}
8 |
9 | import scala.collection.JavaConverters._
10 | import scala.language.postfixOps
11 |
12 | object HeadWorker {
13 |
14 | // sent by HeadAsyncHandler
15 | case class Responded(statusCode: Int, headers: Repox.ResponseHeaders)
16 |
17 | case class Failed(t: Throwable)
18 |
19 | }
20 |
21 | class HeadWorker(val repo: Repo,
22 | val uri: String,
23 | val requestHeaders: FluentCaseInsensitiveStringsMap) extends Actor with ActorLogging {
24 |
25 | import HeadWorker._
26 |
27 | val handler = new HeadAsyncHandler(self, uri, repo)
28 |
29 | context.setReceiveTimeout(Config.headTimeout)
30 | requestHeaders.put(Headers.HOST_STRING, List(repo.host).asJava)
31 |
32 | val (_, client) = Repox.clientOf(repo)
33 |
34 | val requestMethod = if (repo.getOnly) client.prepareGet _ else client.prepareHead _
35 | requestMethod.apply(repo.absolute(uri))
36 | .setHeaders(requestHeaders)
37 | .execute(handler)
38 |
39 | override def receive = {
40 | case Responded(statusCode, headers) =>
41 | handler.cancel()
42 | statusCode match {
43 | case StatusCodes.OK =>
44 | log.debug(s"HeadWorker ${repo.name} 200. $uri")
45 | context.parent ! HeadMaster.FoundIn(repo, headers)
46 | case StatusCodes.NOT_FOUND =>
47 | log.debug(s"HeadWorker ${repo.name} got 404. $uri")
48 | context.parent ! HeadMaster.NotFound(repo)
49 | case _ =>
50 | // server error? further feature may use failed time information
51 | log.debug(s"HeadWorker ${repo.name} got undetermined result $statusCode for $uri.")
52 | context.parent ! HeadMaster.HeadTimeout(repo)
53 | }
54 | self ! PoisonPill
55 | case Failed(t) =>
56 | // further feature may use failed time information
57 | handler.cancel()
58 | log.debug(s"HeadWorker ${repo.name} failed. $uri")
59 | context.parent ! HeadMaster.HeadTimeout(repo)
60 | self ! PoisonPill
61 | case ReceiveTimeout =>
62 | handler.cancel()
63 | log.debug(s"HeadWorker ${repo.name} timeout. $uri")
64 | context.parent ! HeadMaster.HeadTimeout(repo)
65 | self ! PoisonPill
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/HttpHelpers.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import com.gtan.repox.data.Repo
4 | import com.typesafe.scalalogging.LazyLogging
5 | import io.undertow.server.HttpServerExchange
6 | import io.undertow.server.handlers.resource.{ResourceManager, ResourceHandler}
7 | import io.undertow.util.{MimeMappings, Headers, HttpString, StatusCodes}
8 |
9 | trait HttpHelpers { self: LazyLogging =>
10 | type StatusCode = Int
11 | type ResponseHeaders = Map[String, java.util.List[String]]
12 | type HeaderResponse = (Repo, StatusCode, ResponseHeaders)
13 |
14 | def respond404(exchange: HttpServerExchange): Unit = {
15 | exchange.setStatusCode(StatusCodes.NOT_FOUND)
16 | exchange.getResponseChannel // just to avoid mysterious setting Content-length to 0 in endExchange, ugly
17 | exchange.endExchange()
18 | }
19 |
20 | def immediate404(exchange: HttpServerExchange): Unit = {
21 | logger.info(s"Immediate 404 ${exchange.getRequestURI}.")
22 | respond404(exchange)
23 | }
24 |
25 | def smart404(exchange: HttpServerExchange): Unit = {
26 | logger.info(s"Smart 404 ${exchange.getRequestURI}.")
27 | respond404(exchange)
28 | }
29 |
30 | def sendFile(resourceHandler: ResourceHandler, exchange: HttpServerExchange): Unit = {
31 | resourceHandler.handleRequest(exchange)
32 | }
33 |
34 | def immediateFile(resourceHandler: ResourceHandler, exchange: HttpServerExchange): Unit = {
35 | logger.debug(s"Immediate file ${exchange.getRequestURI}")
36 | sendFile(resourceHandler, exchange)
37 | }
38 |
39 | def respondHead(exchange: HttpServerExchange, headers: ResponseHeaders): Unit = {
40 | exchange.setStatusCode(200)
41 | val target = exchange.getResponseHeaders
42 | for ((k, v) <- headers)
43 | target.putAll(new HttpString(k), v)
44 | exchange.getResponseChannel // just to avoid mysterious setting Content-length to 0 in endExchange, ugly
45 | exchange.endExchange()
46 | }
47 |
48 | def immediateHead(resourceManager: ResourceManager, exchange: HttpServerExchange): Unit = {
49 | val uri = exchange.getRequestURI
50 | val resource = resourceManager.getResource(uri)
51 | exchange.setStatusCode(200)
52 | val headers = exchange.getResponseHeaders
53 | headers.put(Headers.CONTENT_LENGTH, resource.getContentLength)
54 | .put(Headers.SERVER, "repox")
55 | .put(Headers.CONNECTION, Headers.KEEP_ALIVE.toString)
56 | .put(Headers.CONTENT_TYPE, resource.getContentType(MimeMappings.DEFAULT))
57 | .put(Headers.LAST_MODIFIED, resource.getLastModifiedString)
58 | exchange.getResponseChannel // just to avoid mysterious setting Content-length to 0 in endExchange, ugly
59 | exchange.endExchange()
60 | logger.debug(s"Immediate head $uri. ")
61 | }
62 |
63 | def redirectTo(exchange: HttpServerExchange, targetLocation: String): Unit = {
64 | exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT)
65 | exchange.getResponseHeaders.put(Headers.LOCATION, targetLocation)
66 | exchange.endExchange()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/JsonSerializer.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.io.NotSerializableException
4 |
5 | import akka.serialization.Serializer
6 | import com.gtan.repox.ExpirationManager.ExpirationSeq
7 | import com.gtan.repox.config._
8 | import com.typesafe.scalalogging.LazyLogging
9 | import play.api.libs.json._
10 |
11 | trait SerializationSupport {
12 | val reader: JsValue => PartialFunction[String, Jsonable]
13 | val writer: PartialFunction[Jsonable, JsValue]
14 | }
15 |
16 | class JsonSerializer extends Serializer with LazyLogging with SerializationSupport {
17 | val ConfigChangedClass = classOf[ConfigChanged].getName
18 |
19 | val serializationSupports: Seq[_ <: SerializationSupport] = Seq(RepoPersister, ProxyPersister, ParameterPersister, Immediate404RulePersister, ExpireRulePersister, ConnectorPersister, ExpirationManager, ConfigPersister)
20 |
21 | override val reader: JsValue => PartialFunction[String, Jsonable] = { jsValue =>
22 | serializationSupports.map(_.reader(jsValue)).reduce(_ orElse _) orElse {
23 | case clazzName: String =>
24 | throw new NotSerializableException(s"No serialization supported for class $clazzName")
25 | }
26 | }
27 | override val writer: PartialFunction[Jsonable, JsValue] = serializationSupports.map(_.writer).reduce(_ orElse _) orElse {
28 | case jsonable: Jsonable =>
29 | throw new NotSerializableException(s"No serialization supported for $jsonable")
30 | }
31 |
32 | override def identifier: Int = 900188
33 |
34 | override def includeManifest: Boolean = false
35 |
36 | override def fromBinary(bytes: Array[Byte], manifest: Option[Class[_]]): AnyRef = manifest match {
37 | case None =>
38 | Json.parse(new String(bytes, "UTF-8")) match {
39 | case obj: JsObject => obj.fields match {
40 | case Seq(
41 | ("manifest", JsString(ConfigChangedClass)),
42 | ("config", config: JsValue),
43 | ("cmd", configcmd: JsValue)) =>
44 | ConfigChanged(configFromJson(config), jsonableFromJson(configcmd))
45 | case _ => jsonableFromJson(obj)
46 | }
47 | case JsString("UseDefault") => UseDefault
48 | case other => jsonableFromJson(other)
49 | }
50 | case Some(_) => throw new NotSerializableException("JsonSerializer does not use extra manifest.")
51 | }
52 |
53 | private def configFromJson(config: JsValue): Config = config.as[Config]
54 |
55 | private def jsonableFromJson(evt: JsValue): Jsonable = evt match {
56 | case obj: JsObject => obj.fields match {
57 | case Seq(
58 | ("manifest", JsString(clazzname)),
59 | ("payload", payload: JsValue)) =>
60 | reader.apply(payload).apply(clazzname)
61 | }
62 | case _ => throw new NotSerializableException(evt.toString())
63 | }
64 |
65 | private def toJson(o: AnyRef): JsValue = o match {
66 | case ConfigChanged(config, cmd) =>
67 | JsObject(
68 | Seq(
69 | "manifest" -> JsString(ConfigChangedClass),
70 | "config" -> Json.toJson(config),
71 | "cmd" -> toJson(cmd)
72 | )
73 | )
74 | case jsonable: Jsonable =>
75 | val payload = writer.apply(jsonable)
76 | JsObject(Seq(
77 | "manifest" -> JsString(jsonable.getClass.getName),
78 | "payload" -> payload
79 | ))
80 | case UseDefault => JsString("UseDefault")
81 | }
82 |
83 | override def toBinary(o: AnyRef): Array[Byte] = toJson(o).toString().getBytes("UTF-8")
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/Main.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 |
4 | import com.gtan.repox.admin.WebConfigHandler
5 | import io.undertow.predicate.Predicates
6 | import io.undertow.server.handlers.{PredicateContextHandler, PredicateHandler}
7 | import io.undertow.server.{HttpHandler, HttpServerExchange}
8 | import io.undertow.{Undertow, UndertowOptions}
9 | import org.xnio.Options
10 |
11 | object Main {
12 | def httpHandlerBridge(realHandler: HttpServerExchange => Unit): HttpHandler = new HttpHandler() {
13 | override def handleRequest(exchange: HttpServerExchange): Unit = {
14 | exchange.dispatch(scala.concurrent.ExecutionContext.Implicits.global.execute, () => realHandler.apply(exchange))
15 | }
16 | }
17 |
18 | def main(args: Array[String]) {
19 | Repox.init()
20 | val server: Undertow = Undertow.builder
21 | .addHttpListener(8078, "0.0.0.0")
22 | .setServerOption[Integer](UndertowOptions.IDLE_TIMEOUT, 1000 * 60 * 30)
23 | .setSocketOption[java.lang.Boolean](Options.KEEP_ALIVE, true)
24 | .setHandler(
25 | new PredicateContextHandler(
26 | new PredicateHandler(
27 | Predicates.prefix("/admin/"),
28 | httpHandlerBridge(WebConfigHandler.handle),
29 | httpHandlerBridge(Repox.handle)
30 | ))
31 | ).build
32 | server.start()
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/Repox.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.nio.file.Paths
4 | import java.util.concurrent.atomic.AtomicLong
5 |
6 | import akka.actor.{ActorSystem, Props}
7 | import akka.agent.Agent
8 | import com.gtan.repox.config.{Config, ConfigPersister, ConfigQuery}
9 | import com.gtan.repox.data.{Connector, ExpireRule, Repo}
10 | import com.ning.http.client.{AsyncHttpClient, ProxyServer => JProxyServer}
11 | import com.typesafe.scalalogging.LazyLogging
12 | import io.undertow.Handlers
13 | import io.undertow.server.HttpServerExchange
14 | import io.undertow.server.handlers.resource.{FileResourceManager, ResourceHandler, ResourceManager}
15 | import io.undertow.util._
16 |
17 | import scala.concurrent.duration._
18 | import scala.language.postfixOps
19 | import scala.util.{Failure, Success, Try}
20 |
21 | object Repox extends LazyLogging with HttpHelpers {
22 | def lookForExpireRule(uri: String): Option[ExpireRule] = Config.expireRules.find(
23 | rule => !rule.disabled && uri.matches(rule.pattern))
24 |
25 |
26 | import concurrent.ExecutionContext.Implicits.global
27 |
28 | val system = ActorSystem("repox")
29 |
30 | private[this] val idGenerator = new AtomicLong(1)
31 |
32 | def nextId: Long = idGenerator.getAndIncrement()
33 |
34 | // uncomment his to view config data modification history
35 | // val configQuery = new ConfigQuery(system)
36 | val configPersister = system.actorOf(Props[ConfigPersister], "ConfigPersister")
37 | val expirationPersister = system.actorOf(Props[ExpirationManager], "ExpirationPersister")
38 | val head404Cache = system.actorOf(Props[Head404Cache], "HeaderCache")
39 | val requestQueueMaster = system.actorOf(Props[RequestQueueMaster], "RequestQueueMaster")
40 |
41 |
42 | val clients: Agent[Map[String, AsyncHttpClient]] = Agent(null)
43 |
44 | def clientOf(repo: Repo): (Connector, AsyncHttpClient) = Config.connectorUsage.find {
45 | case (r, connector) => r.id == repo.id
46 | } match {
47 | case None =>
48 | Config.connectors.find(_.name == "default").get -> clients.get().apply("default")
49 | case Some(Tuple2(r, connector)) =>
50 | println(s"Using client of ${connector.name} for ${repo.name}")
51 | connector -> clients.get().apply(connector.name)
52 | }
53 |
54 | def initClients() = Repox.clients.alter(Config.connectors.map { connector =>
55 | connector.name -> connector.createClient
56 | }.toMap)
57 |
58 | def initResourceManagers() = {
59 | val storage = Repox.storageManager -> Handlers.resource(Repox.storageManager)
60 | val (valid, invalid) = Config.resourceBases.partition { base =>
61 | Paths.get(base).toFile.exists()
62 | }
63 | if (invalid.nonEmpty) {
64 | logger.debug(s"Excluded invalid base(s) (${invalid.mkString(",")})")
65 | }
66 | val extra = for (rb <- valid) yield {
67 | val resourceManager: FileResourceManager = new FileResourceManager(Paths.get(rb).toFile, 100 * 1024)
68 | val resourceHandler = Handlers.resource(resourceManager)
69 | resourceManager -> resourceHandler
70 | }
71 |
72 | Repox.resourceHandlers.alter((extra :+ storage).toMap)
73 | }
74 |
75 | val storageManager = new FileResourceManager(Config.storagePath.toFile, 100 * 1024)
76 |
77 | implicit val timeout = akka.util.Timeout(1 second)
78 |
79 | def isIvyUri(uri: String) = uri.matches( """/[^/]+?\.[^/]+?/.+""")
80 |
81 | def resolveToPaths(uri: String) = {
82 | val removeSlashesAtBeginning: String = uri.dropWhile('/' ==)
83 | (Config.storagePath.resolve(removeSlashesAtBeginning), Config.storagePath.resolve(removeSlashesAtBeginning + ".sha1"))
84 | }
85 |
86 | def orderByPriority(candidates: Seq[Repo]): Seq[Seq[Repo]] =
87 | candidates.groupBy(_.priority).toSeq.sortBy(_._1).map(_._2)
88 |
89 |
90 | /**
91 | * this is the one and only truth
92 | *
93 | * @param uri resource to get or query
94 | * @return
95 | */
96 | def downloaded(uri: String): Option[(ResourceManager, ResourceHandler)] = {
97 | resourceHandlers.get().find { case (resourceManager, handler) =>
98 | resourceManager.getResource(uri.tail) != null
99 | }
100 | }
101 |
102 | private[repox] val MavenFormat = """(/.+)+/((.+?)(_(.+?)(_(.+))?)?)/(.+?)/(\3-\8(-(.+?))?\.(.+))""".r
103 | private[repox] val IvyFormat = """/(.+?)/(.+?)/(scala_(.+?)/)?(sbt_(.+?)/)?(.+?)/(.+?)s/((.+?)(-(.+))?\.(.+))""".r
104 | private[this] val MetaDataFormat = """.+/maven-metadata\.xml""".r
105 | private[this] val MD5Request = """.+\.md5""".r
106 | private[this] val SHA1Request = """(.+)\.sha1""".r
107 | private[this] val supportedScalaVersion = List("2.10", "2.11", "2.12")
108 | private[this] val supportedSbtVersion = List("0.13")
109 |
110 | /**
111 | * transform between uri formats
112 | *
113 | * @param uri
114 | * @return maven format if is ivy format, or ivy format if is maven format
115 | */
116 | def peer(uri: String): Try[List[String]] = uri match {
117 | case MD5Request() =>
118 | Failure(new RuntimeException("We do not support md5 checksum now."))
119 | case SHA1Request(prefix) =>
120 | peer(prefix).map(_.map(_ + ".sha1"))
121 | case MetaDataFormat() =>
122 | if (uri.toUpperCase.endsWith("SNAPSHOT/MAVEN-METADATA.XML"))
123 | Failure(new RuntimeException("SNAPSHOT Request"))
124 | else
125 | Success(Nil)
126 | case MavenFormat(groupIds, _, artifactId, _, scalaVersion, _, sbtVersion, version, fileName, _, classifier, ext) =>
127 | if (version.equalsIgnoreCase("unspecified")) {
128 | Failure(new RuntimeException("Gradle Version-Unspecified Request"))
129 | } else if (version.toUpperCase.endsWith("SNAPSHOT")) {
130 | Failure(new RuntimeException("SNAPSHOT Request"))
131 | } else {
132 | val organization = groupIds.split("/").filter(_.nonEmpty).mkString(".")
133 | val typ = ext match {
134 | case "pom" => "ivy"
135 | case _ => "jar"
136 | }
137 | val peerFile = ext match {
138 | case "pom" => "ivy.xml"
139 | case _ => s"$artifactId.$ext"
140 | }
141 | val result = if (scalaVersion != null && sbtVersion != null) {
142 | s"/$organization/$artifactId/scala_$scalaVersion/sbt_$sbtVersion/$version/${typ}s/$peerFile" :: Nil
143 | } else if (scalaVersion == null && sbtVersion == null) {
144 | val guessedMavenArtifacts = for (scala <- supportedScalaVersion; sbt <- supportedSbtVersion) yield
145 | s"$groupIds/${artifactId}_${scala}_$sbt/$version/$fileName"
146 | s"/$organization/$artifactId/$version/${typ}s/$peerFile" :: guessedMavenArtifacts
147 | } else List(s"/$organization/$artifactId/$version/${typ}s/$peerFile")
148 | Success(result)
149 | }
150 | case IvyFormat(organization, module, _, scalaVersion, _, sbtVersion, revision, typ, fileName, artifact, _, classifier, ext)
151 | =>
152 | val result = if (scalaVersion == null && sbtVersion == null) {
153 | for (scala <- supportedScalaVersion; sbt <- supportedSbtVersion) yield
154 | s"/${organization.split("\\.").mkString("/")}/${module}_${scala}_$sbt/$revision/$module-$revision.$ext"
155 | } else Nil
156 | if (revision.toUpperCase.endsWith("SNAPSHOT")) {
157 | Failure(new RuntimeException("SNAPSHOT Request"))
158 | } else {
159 | Success(result)
160 | }
161 | case _ =>
162 | Failure(new RuntimeException("Invalid Request"))
163 | }
164 |
165 | val resourceHandlers: Agent[Map[FileResourceManager, ResourceHandler]] = Agent(null)
166 |
167 | def handle(exchange: HttpServerExchange): Unit = {
168 | val uri = exchange.getRequestURI
169 | val method = exchange.getRequestMethod
170 | uri match {
171 | case "/" =>
172 | redirectTo(exchange, "/admin/index.html")
173 | case "/favicon.ico" =>
174 | redirectTo(exchange, "/admin/favicon.ico")
175 | case _ =>
176 | Repox.peer(uri) match {
177 | case Success(_) =>
178 | method match {
179 | case Methods.HEAD =>
180 | requestQueueMaster ! Requests.Head(exchange)
181 | case Methods.GET =>
182 | requestQueueMaster ! Requests.Get(exchange)
183 | case _ =>
184 | immediate404(exchange)
185 | }
186 | case Failure(_) =>
187 | Repox.respond404(exchange)
188 | logger.debug(s"Invalid request $method $uri. 404.")
189 | }
190 | }
191 | }
192 |
193 | def init(): Unit = {
194 | configPersister ! 'LoadConfig // this does nothing but eagerly init Repox
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/RequestQueueMaster.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.nio.file.Paths
4 |
5 | import akka.actor._
6 | import com.gtan.repox.config.{ConfigFormats, Config}
7 | import io.undertow.Handlers
8 | import io.undertow.server.handlers.resource.{FileResourceManager, ResourceManager}
9 | import play.api.libs.json.Json
10 |
11 | import scala.util.Random
12 |
13 | case class Queue(method: Symbol, uri: String)
14 |
15 | object RequestQueueMaster {
16 |
17 | // send by ConfigPersister
18 | case object ConfigLoaded
19 |
20 | // send when mainClient and proxyClients are all initialized
21 | case object ClientsInitialized
22 |
23 | // send by QueueWorker
24 | case class KillMe(queue: Queue)
25 |
26 | // send by FileDeleter
27 | case class Quarantine(uri: String)
28 |
29 | case class FileDeleted(uri: String)
30 |
31 | }
32 |
33 | class RequestQueueMaster extends Actor with Stash with ActorLogging with ConfigFormats {
34 |
35 | import RequestQueueMaster._
36 |
37 | import concurrent.ExecutionContext.Implicits.global
38 |
39 | var children = Map.empty[Queue, ActorRef] // Queue -> GetHeadQueueWorker
40 |
41 | var quarantined = Map.empty[String, ActorRef] // Uri -> FileDeleter
42 |
43 | override def receive = initializing
44 |
45 | def initializing: Receive = {
46 | case ConfigLoaded =>
47 | log.info("Config loaded.")
48 | val fut1 = Repox.initClients()
49 | val fut2 = Repox.initResourceManagers()
50 | for (both <- fut1 zip fut2) {
51 | self ! ClientsInitialized
52 | }
53 | case ClientsInitialized =>
54 | log.debug(s"ResourceBases (${Repox.resourceHandlers.get().keys.map(_.getBase).mkString(",")}) initialized.")
55 | log.debug(s"AHC clients (${Repox.clients.get().keys.mkString(",")}) initialized.")
56 | unstashAll()
57 | context become started
58 | case msg =>
59 | log.debug("Repox initializing , stash all msgs...")
60 | stash()
61 | }
62 |
63 | def started: Receive = {
64 | case Quarantine(uri) =>
65 | quarantined.get(uri) match {
66 | case None =>
67 | quarantined = quarantined.updated(uri, sender())
68 | quarantined = quarantined.updated(uri + ".sha1", sender())
69 | sender ! FileDeleter.Quarantined
70 | case _ => // already quarantined , ignore
71 | }
72 | case FileDeleted(uri) =>
73 | quarantined = quarantined - uri - s"$uri.sha1"
74 | // log.debug(s"Redownloading $uri and $uri.sha1")
75 | // self ! Requests.Download(uri, Config.enabledRepos)
76 |
77 | case KillMe(queue) =>
78 | for (worker <- children.get(queue)) {
79 | log.debug(s"RequestQueueMaster stopping worker ${worker.path.name}")
80 | worker ! PoisonPill
81 | }
82 | children = children - queue
83 |
84 | case req@Requests.Get(exchange) =>
85 | val uri = exchange.getRequestURI
86 | quarantined.get(uri) match {
87 | case None =>
88 | val queue = Queue('get, uri)
89 | if (Config.immediate404Rules.filterNot(_.disabled).exists(_.matches(uri))) {
90 | Repox.immediate404(exchange)
91 | for (worker <- children.get(queue)) {
92 | worker ! PoisonPill
93 | }
94 | } else {
95 | for (peers <- Repox.peer(uri)) {
96 | peers.find(p => Repox.downloaded(p).isDefined) match {
97 | case Some(peer) =>
98 | Repox.smart404(exchange)
99 | case None =>
100 | Repox.downloaded(uri) match {
101 | case Some(Tuple2(resourceManager, resourceHandler)) =>
102 | Repox.immediateFile(resourceHandler, exchange)
103 | for (worker <- children.get(queue)) {
104 | worker ! PoisonPill
105 | }
106 | case None =>
107 | children.get(queue) match {
108 | case None =>
109 | val childName = s"GetQueueWorker_${Repox.nextId}"
110 | val worker = context.actorOf(Props(classOf[GetQueueWorker], uri), name = childName)
111 | children = children.updated(Queue('get, uri), worker)
112 | worker ! req
113 | case Some(worker) =>
114 | worker ! req
115 | }
116 | }
117 | }
118 | }
119 | }
120 | case Some(deleter) =>
121 | // file quarantined, 404
122 | Repox.respond404(exchange)
123 | }
124 | case req@Requests.Head(exchange) =>
125 | val uri = exchange.getRequestURI
126 | val queue = Queue('head, uri)
127 | if (Config.immediate404Rules.filterNot(_.disabled).exists(_.matches(uri))) {
128 | Repox.immediate404(exchange)
129 | for (worker <- children.get(queue)) {
130 | worker ! PoisonPill
131 | }
132 | } else {
133 | for (peers <- Repox.peer(uri)) {
134 | peers.find(p => Repox.downloaded(p).isDefined) match {
135 | case Some(peer) =>
136 | Repox.smart404(exchange)
137 | case _ =>
138 | quarantined.get(uri) match {
139 | case None =>
140 | Repox.downloaded(uri) match {
141 | case Some(Tuple2(resourceManager, resourceHandler)) =>
142 | Repox.immediateHead(resourceManager, exchange)
143 | for (worker <- children.get(queue)) {
144 | worker ! PoisonPill
145 | }
146 | case None =>
147 | children.get(queue) match {
148 | case None =>
149 | val workerActorName = s"HeadQueueWorker_${Repox.nextId}"
150 | val worker = context.actorOf(Props(classOf[HeadQueueWorker], uri), workerActorName)
151 | log.debug(s"create HeadQueueWorker $workerActorName")
152 | children = children.updated(queue, worker)
153 | worker ! req
154 | case Some(worker) =>
155 | log.debug(s"Enqueue to HeadQueueWorker ${worker.path.name} $uri")
156 | worker ! req
157 | }
158 | }
159 | case Some(deleter) =>
160 | // file quarantined, 404
161 | Repox.respond404(exchange)
162 | }
163 | }
164 | }
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/Requests.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import com.gtan.repox.data.Repo
4 | import io.undertow.server.HttpServerExchange
5 |
6 | object Requests {
7 | trait Request
8 | case class Get(exchange: HttpServerExchange) extends Request
9 | case class Head(exchange: HttpServerExchange) extends Request
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/TimeoutableFuture.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | import com.typesafe.scalalogging.LazyLogging
6 | import org.jboss.netty.handler.timeout
7 | import org.jboss.netty.handler.timeout.TimeoutException
8 | import org.jboss.netty.util.{Timeout, TimerTask, HashedWheelTimer}
9 |
10 | import scala.concurrent.{ExecutionContext, Future, Promise}
11 | import scala.concurrent.duration.Duration
12 |
13 | /**
14 | * Created by xf on 14/11/20.
15 | */
16 | object TimeoutableFuture extends LazyLogging{
17 | val timer = new HashedWheelTimer(200, TimeUnit.MILLISECONDS)
18 |
19 | private def scheduleTimeout(name: String, promise: Promise[_], after: Duration) = {
20 | timer.newTimeout(new TimerTask {
21 | def run(timeout: Timeout) {
22 | promise.failure(new TimeoutException(s"future $name timeout (${after.toMillis} millis)"))
23 | }
24 | }, after.toNanos, TimeUnit.NANOSECONDS)
25 | }
26 |
27 | def apply[T](fut: Future[T], after: Duration, name: String)(implicit ec: ExecutionContext) = {
28 | val prom = Promise[T]()
29 | val timeout = scheduleTimeout(name, prom, after)
30 | val combinedFut = Future.firstCompletedOf(List(fut, prom.future))
31 | fut onComplete { case result =>
32 | // if(!timeout.isExpired){
33 | // logger.debug(s"timeout for $name canceled because of peer future completion")
34 | // }
35 | timeout.cancel()
36 | }
37 | combinedFut
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/Timestamp.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | /**
4 | * copied from spray-util
5 | */
6 |
7 | import concurrent.duration._
8 |
9 | class Timestamp private(val timestampNanos: Long) extends AnyVal {
10 | def +(period: Duration): Timestamp =
11 | if (isNever) this
12 | else if (!period.isFinite()) Timestamp.never
13 | else new Timestamp(timestampNanos + period.toNanos)
14 |
15 | def -(other: Timestamp): Duration =
16 | if (isNever) Duration.Inf
17 | else if (other.isNever) Duration.MinusInf
18 | else (timestampNanos - other.timestampNanos).nanos
19 |
20 | def isPast: Boolean = System.nanoTime() >= timestampNanos
21 |
22 | def isPast(now: Timestamp): Boolean = now.timestampNanos >= timestampNanos
23 |
24 | def isFuture: Boolean = !isPast
25 |
26 | def isFinite: Boolean = timestampNanos < Long.MaxValue
27 |
28 | def isNever: Boolean = timestampNanos == Long.MaxValue
29 | }
30 |
31 | object Timestamp {
32 | def now: Timestamp = new Timestamp(System.nanoTime())
33 |
34 | def never: Timestamp = new Timestamp(Long.MaxValue)
35 |
36 | implicit val timestampOrdering: Ordering[Timestamp] = new Ordering[Timestamp] {
37 | def compare(x: Timestamp, y: Timestamp): Int =
38 | if (x.timestampNanos < y.timestampNanos) -1 else if (x.timestampNanos == y.timestampNanos) 0 else 1
39 | }
40 | }
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/AuthHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.io.IOException
4 | import java.nio.charset.Charset
5 | import java.util.Date
6 |
7 | import com.gtan.repox.Repox
8 | import com.gtan.repox.config.ConfigPersister.SaveSnapshot
9 | import com.gtan.repox.config._
10 | import com.typesafe.scalalogging.LazyLogging
11 | import io.undertow.io.Receiver.{ErrorCallback, FullStringCallback}
12 | import io.undertow.server.HttpServerExchange
13 | import io.undertow.server.handlers.{CookieImpl, Cookie}
14 | import io.undertow.util._
15 | import play.api.libs.json.Json
16 | import akka.pattern.ask
17 | import concurrent.duration._
18 | import scala.language.postfixOps
19 | import scala.util.{Failure, Success}
20 | import concurrent.ExecutionContext.Implicits.global
21 |
22 | object AuthHandler extends RestHandler with LazyLogging with ConfigFormats {
23 |
24 | import WebConfigHandler._
25 | import ParameterPersister._
26 |
27 | implicit val timeout = akka.util.Timeout(1 second)
28 |
29 | val AUTH_KEY: String = "authenticated"
30 |
31 | private def authenticated(exchange: HttpServerExchange) = {
32 | val cookie = exchange.getRequestCookies.get(AUTH_KEY)
33 | cookie != null && cookie.getValue == "true"
34 | }
35 |
36 | override def route(implicit exchange: HttpServerExchange) = {
37 | val globallyAccessible: PartialFunction[(HttpString, String), Unit] = {
38 | case (Methods.POST, "login") =>
39 | val pass = exchange.getQueryParameters.get("v").getFirst
40 | exchange.setStatusCode(StatusCodes.OK)
41 | if (Config.password == pass) {
42 | exchange.setResponseCookie(new CookieImpl(AUTH_KEY, "true").setPath("/admin"))
43 | exchange.getResponseSender.send( """{"success": true}""")
44 | } else {
45 | exchange.getResponseSender.send( """{"success": false}""")
46 | }
47 | case (Methods.POST, "logout") =>
48 | exchange.setStatusCode(StatusCodes.OK)
49 | exchange.setResponseCookie(new CookieImpl("authenticated", "true").setPath("/admin").setMaxAge(0))
50 | exchange.getRequestCookies.remove("authenticated")
51 | exchange.getResponseChannel
52 | exchange.endExchange()
53 | case (Methods.GET, "exportConfig") =>
54 | exchange.setStatusCode(StatusCodes.OK)
55 | exchange.getResponseHeaders.add(Headers.CONTENT_TYPE, "application/force-download")
56 | exchange.getResponseHeaders.add(Headers.CONTENT_DISPOSITION, """attachment; filename="repox.config.json""")
57 | exchange.getResponseSender.send(Json.toJson(Config.get.copy(password = "not exported")).toString)
58 | }
59 | val needAuth: PartialFunction[(HttpString, String), Unit] = {
60 | case _ if !authenticated(exchange) =>
61 | exchange.setStatusCode(StatusCodes.FORBIDDEN)
62 | exchange.endExchange()
63 | }
64 | globallyAccessible orElse needAuth orElse {
65 | case (Methods.POST, "saveSnapshot") =>
66 | (Repox.configPersister ? SaveSnapshot).onComplete {
67 | result =>
68 | exchange.getResponseSender.send( s"""{"success": ${result.isSuccess}}""")
69 | }
70 | case (Methods.PUT, "importConfig") =>
71 | val contentType = exchange.getRequestHeaders.getFirst(Headers.CONTENT_TYPE)
72 | if (contentType.startsWith("application/json")) {
73 | val splitted = contentType.split("charset=")
74 | val charset = if(splitted.length == 2) Charset.forName(splitted(1)) else Charset.forName("UTF-8")
75 | exchange.getRequestReceiver.receiveFullString(
76 | new FullStringCallback {
77 | override def handle(exchange: HttpServerExchange, message: String): Unit = {
78 | val uploaded = Json.parse(message).as[Config]
79 | setConfigAndRespond(exchange, Repox.configPersister ? ImportConfig(uploaded))
80 | }
81 | }, new ErrorCallback {
82 | override def error(exchange: HttpServerExchange, e: IOException): Unit = {
83 | exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
84 | exchange.endExchange()
85 | }
86 | }, charset)
87 | } else {
88 | exchange.setStatusCode(StatusCodes.BAD_REQUEST)
89 | exchange.endExchange()
90 | }
91 | case (Methods.PUT, "password") =>
92 | val v = exchange.getQueryParameters.get("v").getFirst
93 | val json = Json.parse(v)
94 | val (p1, p2) = ((json \ "p1").as[String], (json \ "p2").as[String])
95 | if (p1 == p2) {
96 | setConfigAndRespond(exchange, Repox.configPersister ? ModifyPassword(p1))
97 | }
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ConnectorVO.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import com.gtan.repox.config.Config
4 | import com.gtan.repox.data.{Connector, ProxyServer}
5 | import play.api.libs.json.Json
6 |
7 | case class ConnectorVO(connector: Connector, proxy: Option[ProxyServer]) {
8 | }
9 |
10 | object ConnectorVO {
11 | def wrap(connector: Connector) = ConnectorVO(connector, Config.proxyUsage.get(connector))
12 |
13 | implicit val format = Json.format[ConnectorVO]
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ConnectorsHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.net.URLDecoder
4 |
5 | import akka.pattern.ask
6 | import com.gtan.repox.Repox
7 | import com.gtan.repox.config.{Config, ConfigPersister}
8 | import com.gtan.repox.data.Connector
9 | import io.undertow.server.HttpServerExchange
10 | import io.undertow.util.{HttpString, Methods}
11 | import play.api.libs.json.{JsObject, Json}
12 |
13 | import scala.concurrent.duration._
14 | import scala.language.postfixOps
15 |
16 | object ConnectorsHandler extends RestHandler {
17 |
18 | import com.gtan.repox.admin.WebConfigHandler._
19 | import com.gtan.repox.config.ConnectorPersister._
20 |
21 | implicit val timeout = akka.util.Timeout(1 second)
22 |
23 | override def route(implicit exchange: HttpServerExchange): PartialFunction[(HttpString, String), Unit] = {
24 | case (Methods.GET, "connectors") =>
25 | val config = Config.get
26 | respondJson(exchange, Json.obj(
27 | "connectors" -> config.connectors.map(ConnectorVO.wrap),
28 | "proxies" -> config.proxies)
29 | )
30 | case (Methods.POST, "connector") =>
31 | val newV = exchange.getQueryParameters.get("v").getFirst
32 | val vo = Json.parse(newV).as[ConnectorVO]
33 | setConfigAndRespond(exchange, Repox.configPersister ? NewConnector(vo))
34 | case (Methods.PUT, "connector") =>
35 | val newV = exchange.getQueryParameters.get("v").getFirst
36 | val vo = Json.parse(newV).as[ConnectorVO]
37 | setConfigAndRespond(exchange, Repox.configPersister ? UpdateConnector(vo))
38 | case (Methods.DELETE, "connector") =>
39 | val newV = exchange.getQueryParameters.get("v").getFirst
40 | setConfigAndRespond(exchange, Repox.configPersister ? DeleteConnector(newV.toLong))
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ExpireRulesHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.net.URLDecoder
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import com.gtan.repox.data.ExpireRule
8 | import io.undertow.server.HttpServerExchange
9 | import io.undertow.util.Methods
10 | import play.api.libs.json.Json
11 | import akka.pattern.ask
12 | import concurrent.duration._
13 | import com.gtan.repox.config.ExpireRulePersister._
14 |
15 | import collection.JavaConverters._
16 |
17 | object ExpireRulesHandler extends RestHandler {
18 | implicit val timeout = akka.util.Timeout(5 seconds)
19 |
20 | import com.gtan.repox.admin.WebConfigHandler._
21 |
22 | override def route(implicit exchange: HttpServerExchange) = {
23 | case (Methods.GET, "expireRules") =>
24 | respondJson(exchange, Config.expireRules)
25 | case (Methods.POST, "expireRule") | (Methods.PUT, "expireRule") =>
26 | val newV = exchange.getQueryParameters.get("v").getFirst
27 | val rule = Json.parse(newV).as[ExpireRule]
28 | setConfigAndRespond(exchange, Repox.configPersister ? NewOrUpdateExpireRule(rule))
29 | case (Methods.PUT, "expireRule/enable") =>
30 | val newV = exchange.getQueryParameters.get("v").getFirst
31 | setConfigAndRespond(exchange, Repox.configPersister ? EnableExpireRule(newV.toLong))
32 | case (Methods.PUT, "expireRule/disable") =>
33 | val newV = exchange.getQueryParameters.get("v").getFirst
34 | setConfigAndRespond(exchange, Repox.configPersister ? DisableExpireRule(newV.toLong))
35 | case (Methods.DELETE, "expireRule") =>
36 | val newV = exchange.getQueryParameters.get("v").getFirst
37 | setConfigAndRespond(exchange, Repox.configPersister ? DeleteExpireRule(newV.toLong))
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/Immediate404RulesHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.net.URLDecoder
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import com.gtan.repox.config.Immediate404RulePersister._
8 | import com.gtan.repox.data.Immediate404Rule
9 | import io.undertow.server.HttpServerExchange
10 | import io.undertow.util.Methods
11 | import play.api.libs.json.Json
12 | import akka.pattern.ask
13 | import concurrent.duration._
14 |
15 | object Immediate404RulesHandler extends RestHandler {
16 | implicit val timeout = akka.util.Timeout(5 seconds)
17 |
18 | import com.gtan.repox.admin.WebConfigHandler._
19 |
20 | override def route(implicit exchange: HttpServerExchange) = {
21 | case (Methods.GET, "immediate404Rules") =>
22 | respondJson(exchange, Config.immediate404Rules)
23 | case (Methods.POST, "immediate404Rule") | (Methods.PUT, "immediate404Rule") =>
24 | val newV = exchange.getQueryParameters.get("v").getFirst
25 | val rule = Json.parse(newV).as[Immediate404Rule]
26 | setConfigAndRespond(exchange, Repox.configPersister ? NewOrUpdateImmediate404Rule(rule))
27 | case (Methods.PUT, "immediate404Rule/enable") =>
28 | val newV = exchange.getQueryParameters.get("v").getFirst
29 | setConfigAndRespond(exchange, Repox.configPersister ? EnableImmediate404Rule(newV.toLong))
30 | case (Methods.PUT, "immediate404Rule/disable") =>
31 | val newV = exchange.getQueryParameters.get("v").getFirst
32 | setConfigAndRespond(exchange, Repox.configPersister ? DisableImmediate404Rule(newV.toLong))
33 | case (Methods.DELETE, "immediate404Rule") =>
34 | val newV = exchange.getQueryParameters.get("v").getFirst
35 | setConfigAndRespond(exchange, Repox.configPersister ? DeleteImmediate404Rule(newV.toLong))
36 |
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ParametersHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import com.gtan.repox.Repox
4 | import com.gtan.repox.config.Config
5 | import com.gtan.repox.config.ParameterPersister._
6 | import io.undertow.server.HttpServerExchange
7 | import io.undertow.util.Methods
8 | import play.api.libs.json.{Json, JsNumber, JsString, JsObject}
9 | import collection.JavaConverters._
10 | import scala.concurrent.duration._
11 | import akka.pattern.ask
12 |
13 | import scala.language.postfixOps
14 |
15 | object ParametersHandler extends RestHandler {
16 |
17 | import WebConfigHandler._
18 |
19 | implicit val timeout = akka.util.Timeout(1 second)
20 |
21 | override def route(implicit exchange: HttpServerExchange) = {
22 | case (Methods.GET, "parameters") =>
23 | val config = Config.get
24 | respondJson(exchange, Seq(
25 | Json.obj("name" -> "headRetryTimes", "value" -> config.headRetryTimes),
26 | Json.obj("name" -> "headTimeout", "value" -> config.headTimeout.toSeconds, "unit" -> "s"),
27 | Json.obj("name" -> "extraResources", "value" -> config.extraResources)
28 | ))
29 | case (Methods.PUT, "headTimeout") =>
30 | val newV = exchange.getQueryParameters.get("v").getFirst
31 | setConfigAndRespond(exchange,
32 | Repox.configPersister ? SetHeadTimeout(Duration.apply(newV.toInt, SECONDS)))
33 | case (Methods.PUT, "headRetryTimes") =>
34 | val newV = exchange.getQueryParameters.get("v").getFirst
35 | setConfigAndRespond(exchange,
36 | Repox.configPersister ? SetHeadRetryTimes(newV.toInt))
37 | case (Methods.PUT, "extraResources") =>
38 | val newV = exchange.getQueryParameters.get("v").getFirst
39 | setConfigAndRespond(exchange,
40 | Repox.configPersister ? SetExtraResources(newV))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ProxiesHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.net.URLDecoder
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import com.gtan.repox.config.ProxyPersister._
8 | import com.gtan.repox.data.ProxyServer
9 | import io.undertow.server.HttpServerExchange
10 | import io.undertow.util.{HttpString, Methods}
11 | import play.api.libs.json.Json
12 | import collection.JavaConverters._
13 | import akka.pattern.ask
14 | import concurrent.duration._
15 |
16 | object ProxiesHandler extends RestHandler {
17 | implicit val timeout = akka.util.Timeout(5 seconds)
18 |
19 | import WebConfigHandler._
20 |
21 | override def route(implicit exchange: HttpServerExchange): PartialFunction[(HttpString, String), Unit] = {
22 | case (Methods.GET, "proxies") =>
23 | respondJson(exchange, Config.proxies)
24 | case (Methods.POST, "proxy") | (Methods.PUT, "proxy") =>
25 | val newV = exchange.getQueryParameters.get("v").getFirst
26 | val proxy = Json.parse(newV).as[ProxyServer]
27 | setConfigAndRespond(exchange, Repox.configPersister ? NewOrUpdateProxy(proxy))
28 | case (Methods.PUT, "proxy/enable") =>
29 | val newV = exchange.getQueryParameters.get("v").getFirst
30 | setConfigAndRespond(exchange, Repox.configPersister ? EnableProxy(newV.toLong))
31 | case (Methods.PUT, "proxy/disable") =>
32 | val newV = exchange.getQueryParameters.get("v").getFirst
33 | setConfigAndRespond(exchange, Repox.configPersister ? DisableProxy(newV.toLong))
34 | case (Methods.DELETE, "proxy") =>
35 | val newV = exchange.getQueryParameters.get("v").getFirst
36 | setConfigAndRespond(exchange, Repox.configPersister ? DeleteProxy(newV.toLong))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/RepoVO.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import com.gtan.repox.Repox
4 | import com.gtan.repox.config.Config
5 | import com.gtan.repox.data.{DurationFormat, Connector, ProxyServer, Repo}
6 | import com.ning.http.client.{ProxyServer => JProxyServer}
7 | import play.api.libs.json.Json
8 |
9 | import collection.JavaConverters._
10 |
11 | case class RepoVO(repo: Repo, connector: Option[Connector])
12 |
13 | object RepoVO {
14 | def wrap(repo: Repo): RepoVO = RepoVO(repo, Config.connectorUsage.get(repo))
15 |
16 | implicit val format = Json.format[RepoVO]
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/ResetHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import com.gtan.repox.Repox
4 | import com.typesafe.scalalogging.LazyLogging
5 | import io.undertow.server.HttpServerExchange
6 | import io.undertow.util.Methods
7 |
8 | object ResetHandler extends RestHandler with LazyLogging{
9 | import WebConfigHandler._
10 |
11 | override def route(implicit exchange: HttpServerExchange) = {
12 | case (Methods.POST, "resetMainClient") =>
13 | // setConfigAndRespond(exchange, Repox.mainClient.alter(Repox.createMainClient))
14 | case (Methods.POST, "resetProxyClients") =>
15 | // setConfigAndRespond(exchange, Repox.proxyClients.alter(Repox.createProxyClients))
16 | case _ =>
17 | logger.debug(s"Invalid Request. ${exchange.getRequestURI}")
18 | Repox.respond404(exchange)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/RestHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import io.undertow.server.HttpServerExchange
4 | import io.undertow.util.HttpString
5 |
6 | trait RestHandler {
7 | def route(implicit exchange: HttpServerExchange): PartialFunction[(HttpString, String), Unit]
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/StaticAssetHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import io.undertow.Handlers
4 | import io.undertow.server.HttpServerExchange
5 | import io.undertow.server.handlers.resource.ClassPathResourceManager
6 | import io.undertow.util.Methods
7 |
8 | object StaticAssetHandler extends RestHandler {
9 |
10 | import WebConfigHandler._
11 |
12 | override def route(implicit exchange: HttpServerExchange) = {
13 | case (Methods.GET, target) if isStaticRequest(target) =>
14 | Handlers
15 | .resource(new ClassPathResourceManager(this.getClass.getClassLoader))
16 | .handleRequest(exchange)
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/UpstreamsHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import akka.pattern.ask
4 | import com.gtan.repox.Repox
5 | import com.gtan.repox.config.Config
6 | import com.gtan.repox.config.RepoPersister._
7 | import io.undertow.server.HttpServerExchange
8 | import io.undertow.util.{HttpString, Methods}
9 | import play.api.libs.json.Json
10 |
11 | import scala.concurrent.duration._
12 | import scala.language.postfixOps
13 |
14 | object UpstreamsHandler extends RestHandler {
15 |
16 | import WebConfigHandler._
17 |
18 | implicit val timeout = akka.util.Timeout(1 second)
19 |
20 | override def route(implicit exchange: HttpServerExchange): PartialFunction[(HttpString, String), Unit] = {
21 | case (Methods.GET, "upstreams") =>
22 | val config = Config.get
23 | respondJson(exchange, Json.obj(
24 | "upstreams" -> config.repos.sortBy(_.priority).map(RepoVO.wrap),
25 | "connectors" -> config.connectors.filterNot(_.name == "default")
26 | ))
27 |
28 | case (Methods.POST, "upstream") =>
29 | val newV = exchange.getQueryParameters.get("v").getFirst
30 | val vo = Json.parse(newV).as[RepoVO]
31 | setConfigAndRespond(exchange, Repox.configPersister ? NewRepo(vo))
32 | case (Methods.POST, "upstream/up") =>
33 | val id = exchange.getQueryParameters.get("v").getFirst.toLong
34 | setConfigAndRespond(exchange, Repox.configPersister ? MoveUpRepo(id))
35 | case (Methods.POST, "upstream/down") =>
36 | val id = exchange.getQueryParameters.get("v").getFirst.toLong
37 | setConfigAndRespond(exchange, Repox.configPersister ? MoveDownRepo(id))
38 | case (Methods.PUT, "upstream") =>
39 | val newV = exchange.getQueryParameters.get("v").getFirst
40 | val vo = Json.parse(newV).as[RepoVO]
41 | setConfigAndRespond(exchange, Repox.configPersister ? UpdateRepo(vo))
42 | case (Methods.PUT, "upstream/disable") =>
43 | val id = exchange.getQueryParameters.get("v").getFirst.toLong
44 | setConfigAndRespond(exchange, Repox.configPersister ? DisableRepo(id))
45 | case (Methods.PUT, "upstream/enable") =>
46 | val id = exchange.getQueryParameters.get("v").getFirst.toLong
47 | setConfigAndRespond(exchange, Repox.configPersister ? EnableRepo(id))
48 | case (Methods.DELETE, "upstream") =>
49 | val newV = exchange.getQueryParameters.get("v").getFirst
50 | setConfigAndRespond(exchange, Repox.configPersister ? DeleteRepo(newV.toLong))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/admin/WebConfigHandler.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.admin
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.charset.Charset
5 |
6 | import io.undertow.server.HttpServerExchange
7 | import io.undertow.util.{Headers, StatusCodes}
8 | import play.api.libs.json.Format
9 |
10 | import scala.concurrent.ExecutionContext.Implicits.global
11 | import scala.concurrent.Future
12 | import scala.language.postfixOps
13 | import scala.util.{Failure, Success}
14 |
15 |
16 | object WebConfigHandler {
17 | def handle(httpServerExchange: HttpServerExchange) = {
18 | implicit val exchange = httpServerExchange
19 | val (method, uriUnprefixed) = (
20 | httpServerExchange.getRequestMethod,
21 | httpServerExchange.getRequestURI.drop("/admin/".length))
22 | restHandlers.map(_.route).reduce(_ orElse _).apply(method -> uriUnprefixed)
23 | }
24 |
25 | val restHandlers: Seq[RestHandler] = List(
26 | StaticAssetHandler,
27 | AuthHandler,
28 | UpstreamsHandler,
29 | ConnectorsHandler,
30 | ProxiesHandler,
31 | Immediate404RulesHandler,
32 | ExpireRulesHandler,
33 | ParametersHandler,
34 | ResetHandler
35 | )
36 |
37 | def setConfigAndRespond(exchange: HttpServerExchange, result: Future[Any]): Unit = {
38 | result.onComplete {
39 | case Success(_) =>
40 | respondEmptyOK(exchange)
41 | case Failure(t) =>
42 | respondError(exchange, t)
43 | }
44 | }
45 |
46 | def isStaticRequest(target: String) = Set(".html", ".css", ".js", ".json", ".ico", ".ttf", ".map", "woff", "woff2", ".svg", "otf", "png", "jpg", "gif").exists(target.endsWith)
47 |
48 | def respondJson[T: Format](exchange: HttpServerExchange, data: T): Unit = {
49 | exchange.setStatusCode(StatusCodes.OK)
50 | val respondHeaders = exchange.getResponseHeaders
51 | respondHeaders.put(Headers.CONTENT_TYPE, "application/json")
52 | val json = implicitly[Format[T]].writes(data)
53 | exchange.getResponseChannel.writeFinal(ByteBuffer.wrap(json.toString().getBytes(Charset.forName("UTF-8"))))
54 | exchange.endExchange()
55 | }
56 |
57 | def respondError(exchange: HttpServerExchange, t: Throwable): Unit = {
58 | exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
59 | exchange.getResponseChannel
60 | exchange.endExchange()
61 | }
62 |
63 | def respondEmptyOK(exchange: HttpServerExchange): Unit = {
64 | exchange.setStatusCode(StatusCodes.OK)
65 | exchange.getResponseChannel
66 | exchange.endExchange()
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/Config.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import java.nio.file.Paths
4 |
5 | import akka.agent.Agent
6 | import com.gtan.repox.data._
7 | import com.ning.http.client.{ProxyServer => JProxyServer}
8 | import com.typesafe.scalalogging.LazyLogging
9 | import play.api.libs.json.Json
10 |
11 | import scala.concurrent.ExecutionContext.Implicits.global
12 | import scala.concurrent.Future
13 | import scala.concurrent.duration._
14 | import scala.language.postfixOps
15 | import scala.util.Properties.userHome
16 |
17 | case class Config(proxies: Seq[ProxyServer],
18 | repos: IndexedSeq[Repo],
19 | connectorUsage: Map[Repo, Connector],
20 | proxyUsage: Map[Connector, ProxyServer],
21 | immediate404Rules: Seq[Immediate404Rule],
22 | expireRules: Seq[ExpireRule],
23 | connectors: Set[Connector],
24 | headTimeout: Duration,
25 | headRetryTimes: Int,
26 | password: String,
27 | extraResources: Seq[String]) extends Jsonable
28 |
29 | object Config extends LazyLogging with ConfigFormats {
30 |
31 | val defaultProxies = List(
32 | ProxyServer(id = Some(1), name = "Lantern", protocol = JProxyServer.Protocol.HTTP, host = "localhost", port = 8787)
33 | )
34 | val defaultConnectors = Set(
35 | Connector(id = Some(1),
36 | name = "default",
37 | connectionTimeout = 5 seconds,
38 | connectionIdleTimeout = 10 seconds,
39 | maxConnections = 40,
40 | maxConnectionsPerHost = 20),
41 | Connector(id = Some(2),
42 | name = "fast-upstream",
43 | connectionTimeout = 5 seconds,
44 | connectionIdleTimeout = 5 seconds,
45 | maxConnections = 40,
46 | maxConnectionsPerHost = 20
47 | ),
48 | Connector(id = Some(3),
49 | name = "slow-upstream",
50 | connectionTimeout = 5 seconds,
51 | connectionIdleTimeout = 40 seconds,
52 | maxConnections = 40,
53 | maxConnectionsPerHost = 20
54 | )
55 | )
56 | val defaultRepos: IndexedSeq[Repo] = IndexedSeq(
57 | Repo(Some(1), "koala", "http://nexus.openkoala.org/content/groups/Koala-release",
58 | priority = 1, getOnly = true, maven = true),
59 | Repo(Some(3), "sonatype", "http://oss.sonatype.org/content/repositories/releases", priority = 2),
60 | Repo(Some(4), "typesafe", "http://repo.typesafe.com/typesafe/releases", priority = 2),
61 | Repo(Some(2), "oschina", "http://maven.oschina.net/content/groups/public",
62 | priority = 2, getOnly = true, maven = true, parentId = Some(8)),
63 | Repo(Some(5), "sbt-plugin", "http://dl.bintray.com/sbt/sbt-plugin-releases", priority = 4),
64 | Repo(Some(6), "scalaz", "http://dl.bintray.com/scalaz/releases", priority = 4),
65 | Repo(Some(9), "scalajs", "http://dl.bintray.com/content/scala-js/scala-js-releases", priority = 4),
66 | Repo(Some(7), "central", "http://repo1.maven.org/maven2", priority = 4, maven = true),
67 | Repo(Some(8), "ibiblio", "http://mirrors.ibiblio.org/maven2", priority = 5, maven = true, disabled = true)
68 | )
69 |
70 |
71 | val defaultImmediate404Rules: Seq[Immediate404Rule] = Vector(
72 | Immediate404Rule(Some(1), """.+-javadoc\.jar"""), // we don't want javadoc
73 | Immediate404Rule(Some(2), """.+-parent.*\.jar"""), // parent have no jar
74 | Immediate404Rule(Some(3), """(/.+)+/((.+?-project)(_(.+?)(_(.+))?)?)/(.+?)/\3-\8(-(.+?))?\.jar"""), // maven x-project have no jar
75 | Immediate404Rule(Some(4), """(/.+)+/((.+?-pom)(_(.+?)(_(.+))?)?)/(.+?)/\3-\8(-(.+?))?\.jar"""), // maven x-pom have no jar
76 | Immediate404Rule(Some(5), """/.+?/(.+?-project)/.+/\1\.jar"""), // ivy x-project have no jar
77 | Immediate404Rule(Some(6), """/org/jboss/xnio/xnio-all/.+\.jar"""),
78 | Immediate404Rule(Some(7), """/org\.jboss\.xnio/xnio-all/.+\.jar"""),
79 | Immediate404Rule(Some(8), """/org/apache/apache/(\d+)/.+\.jar"""),
80 | Immediate404Rule(Some(9), """/org\.apache/apache/(\d+)/.+\.jar"""),
81 | Immediate404Rule(Some(10), """/com/google/google/(\d+)/.+\.jar"""),
82 | Immediate404Rule(Some(11), """/com\.google/google/(\d+)/.+\.jar"""),
83 | Immediate404Rule(Some(12), """/org/ow2/ow2/.+\.jar"""),
84 | Immediate404Rule(Some(13), """/org\.ow2/ow2/.+\.jar"""),
85 | Immediate404Rule(Some(14), """(/.+)+/((.+?-site)(_(.+?)(_(.+))?)?)/(.+?)/\3-\8(-(.+?))?\.jar"""), // maven x-site have no jar
86 | Immediate404Rule(Some(15),
87 | """/.+?/(.+?-site)/.+/\1\.jar""", // ivy x-site have no jar
88 | Some( """/com\.typesafe\.sbt/sbt-site/.*""")), // except 'sbt-site', being used by akka
89 | Immediate404Rule(Some(16), """/org/fusesource/leveldbjni/.+-sources\.jar"""),
90 | Immediate404Rule(Some(17), """/org\.fusesource\.leveldbjni/.+-sources\.jar"""),
91 | Immediate404Rule(Some(18), """/.+?/(.+?-pom)/.+/\1\.jar"""), // ivy x-pom have no jar
92 | Immediate404Rule(Some(19), """/org/webjars/.+-sources.jar"""), // webjars have no sources
93 | Immediate404Rule(Some(20), """/org\.webjars/.+-sources.jar""")
94 | )
95 |
96 | def defaultExpireRules = Seq(
97 | ExpireRule(Some(1), ".+/maven-metadata.xml", 1 day)
98 | )
99 |
100 | implicit class string2Repo(repoName: String) {
101 | def use(connectorName: String) =
102 | defaultRepos.find(_.name == repoName).get -> defaultConnectors.find(_.name == connectorName).get
103 | }
104 |
105 | val default = Config(
106 | proxies = defaultProxies,
107 | repos = defaultRepos,
108 | connectorUsage = Map(
109 | "koala" use "fast-upstream",
110 | "typesafe" use "slow-upstream",
111 | "oschina" use "fast-upstream",
112 | "scalajs" use "slow-upstream"
113 | ),
114 | proxyUsage = Map(),
115 | immediate404Rules = defaultImmediate404Rules,
116 | expireRules = defaultExpireRules,
117 | connectors = defaultConnectors,
118 | headTimeout = 3 seconds,
119 | headRetryTimes = 3,
120 | password = "zhimakaimen",
121 | extraResources = Seq(Paths.get(userHome, ".m2", "repository").toString)
122 | )
123 |
124 | private[this] val instance: Agent[Config] = Agent[Config](null)
125 |
126 | def set(data: Config): Future[Config] = instance.alter(data)
127 |
128 | def get = instance.get()
129 |
130 | val storagePath = Paths.get(userHome, ".repox", "storage")
131 |
132 | def repos: Seq[Repo] = instance.get().repos
133 |
134 | def enabledRepos: Seq[Repo] = repos.filterNot(_.disabled)
135 |
136 | def proxies: Seq[ProxyServer] = instance.get().proxies
137 |
138 | def enabledProxies: Seq[ProxyServer] = proxies.filterNot(_.disabled)
139 |
140 | def connectorUsage: Map[Repo, Connector] = instance.get().connectorUsage
141 |
142 | def proxyUsage: Map[Connector, ProxyServer] = instance.get().proxyUsage
143 |
144 | def immediate404Rules: Seq[Immediate404Rule] = instance.get().immediate404Rules
145 |
146 | def enabledImmediate404Rules: Seq[Immediate404Rule] = immediate404Rules.filterNot(_.disabled)
147 |
148 | def expireRules: Seq[ExpireRule] = instance.get().expireRules
149 |
150 | def enabledExpireRules: Seq[ExpireRule] = expireRules.filterNot(_.disabled)
151 |
152 | def password: String = instance.get().password
153 |
154 | def headRetryTimes: Int = instance.get().headRetryTimes
155 |
156 | def headTimeout: Duration = instance.get().headTimeout
157 |
158 | def connectors: Set[Connector] = instance.get().connectors
159 |
160 | def extraResources: Seq[String] = instance.get().extraResources
161 |
162 | def resourceBases: Seq[String] = extraResources :+ storagePath.toString
163 | }
164 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ConfigFormats.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.data.{Connector, ProxyServer, Repo}
4 | import play.api.libs.json._
5 |
6 | trait ConfigFormats {
7 | implicit val connectorUsageFormat = new Format[Map[Repo, Connector]] {
8 | override def writes(o: Map[Repo, Connector]) = JsArray(o.map {
9 | case (repo, connector) => JsObject(
10 | Seq(
11 | "repo" -> Json.toJson(repo),
12 | "connector" -> Json.toJson(connector)
13 | )
14 | )
15 | }.toSeq)
16 |
17 | override def reads(json: JsValue): JsResult[Map[Repo, Connector]] = try {
18 | (json: @unchecked) match {
19 | case JsArray(values) =>
20 | JsSuccess(values.map { value =>
21 | (value: @unchecked) match {
22 | case obj: JsObject => (obj.fields: @unchecked) match {
23 | case Seq(
24 | ("repo", repoJsVal: JsValue),
25 | ("connector", connectorJsVal: JsValue)) =>
26 | repoJsVal.as[Repo] -> connectorJsVal.as[Connector]
27 | }
28 | }
29 | } toMap)
30 | }
31 | } catch {
32 | case e: MatchError => JsError(s"Config.connectorUsage deserialize from json failed. $e")
33 | }
34 | }
35 |
36 | implicit val proxyUsageFormat = new Format[Map[Connector, ProxyServer]] {
37 | override def writes(o: Map[Connector, ProxyServer]) = JsArray(o.map {
38 | case (connector, proxy) => JsObject(
39 | Seq(
40 | "connector" -> Json.toJson(connector),
41 | "proxy" -> Json.toJson(proxy)
42 | )
43 | )
44 | }.toSeq)
45 |
46 | override def reads(json: JsValue): JsResult[Map[Connector, ProxyServer]] = try {
47 | (json: @unchecked) match {
48 | case JsArray(values) =>
49 | JsSuccess(values.map { value =>
50 | (value: @unchecked) match {
51 | case obj: JsObject => (obj.fields: @unchecked) match {
52 | case Seq(
53 | ("connector", connectorJsVal: JsValue),
54 | ("proxy", proxyJsVal: JsValue)) =>
55 | connectorJsVal.as[Connector] -> proxyJsVal.as[ProxyServer]
56 | }
57 | }
58 | } toMap)
59 | }
60 | } catch {
61 | case e: MatchError => JsError(s"Config.proxyUsage deserialize from json failed. $e")
62 | }
63 | }
64 |
65 | import com.gtan.repox.data.DurationFormat._
66 |
67 | implicit val format = Json.format[Config]
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ConfigPersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import java.nio.file.Paths
4 |
5 | import akka.actor.{Status, ActorLogging, ActorRef}
6 | import akka.persistence._
7 | import com.gtan.repox.config.ConfigPersister.SaveSnapshot
8 | import com.gtan.repox.{SerializationSupport, Repox, RequestQueueMaster}
9 | import com.ning.http.client.{AsyncHttpClient, ProxyServer => JProxyServer}
10 | import io.undertow.Handlers
11 | import io.undertow.server.handlers.resource.{FileResourceManager, ResourceManager}
12 | import io.undertow.util.StatusCodes
13 | import play.api.libs.json.{Json, JsValue}
14 |
15 | import scala.concurrent.ExecutionContext.Implicits.global
16 | import scala.concurrent.Future
17 |
18 | trait Jsonable
19 |
20 | trait ConfigCmd extends Jsonable {
21 | def transform(old: Config): Config = old
22 | }
23 |
24 | case class ImportConfig(uploaded: Config) extends ConfigCmd {
25 | override def transform(old: Config): Config = uploaded.copy(password = old.password)
26 | }
27 |
28 | object ImportConfig {
29 | implicit val formats = Json.format[ImportConfig]
30 | }
31 |
32 | // Everything can be command or Jsonable, but only Evt will be persisted.
33 | trait Evt
34 |
35 | case class ConfigChanged(config: Config, configCmd: Jsonable) extends Evt
36 |
37 | case object UseDefault extends Evt
38 |
39 | object ConfigPersister extends SerializationSupport {
40 |
41 | case object SaveSnapshot
42 |
43 | val ConfigClass = classOf[Config].getName
44 | val ImportConfigClass = classOf[ImportConfig].getName
45 |
46 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
47 | case ConfigClass => payload.as[Config]
48 | case ImportConfigClass => payload.as[ImportConfig]
49 | }
50 |
51 | override val writer: PartialFunction[Jsonable, JsValue] = {
52 | case o: Config => Json.toJson(o)
53 | case o: ImportConfig => Json.toJson(o)
54 | }
55 | }
56 |
57 | class ConfigPersister extends PersistentActor with ActorLogging {
58 |
59 | import com.gtan.repox.config.ConnectorPersister._
60 | import com.gtan.repox.config.ParameterPersister._
61 |
62 |
63 | override def persistenceId = "Config"
64 |
65 | var config: Config = _
66 | var saveSnapshotRequester: Option[ActorRef] = None
67 |
68 | def onConfigSaved(sender: ActorRef, c: ConfigChanged) = {
69 | log.debug(s"event caused by cmd: ${c.configCmd}")
70 | config = c.config
71 | for {
72 | _ <- Config.set(config)
73 | _ <- c.configCmd match {
74 | case NewConnector(vo) =>
75 | Repox.clients.alter { clients =>
76 | clients.updated(vo.connector.name, vo.connector.createClient)
77 | }
78 | case DeleteConnector(id) =>
79 | Repox.clients.alter { clients =>
80 | Config.connectors.find(_.id.contains(id)).fold(clients) { connector =>
81 | for (client <- clients.get(connector.name)) {
82 | client.closeAsynchronously()
83 | }
84 | clients - connector.name
85 | }
86 | }
87 | case UpdateConnector(vo) =>
88 | Repox.clients.alter { clients =>
89 | for (client <- clients.get(vo.connector.name)) {
90 | client.closeAsynchronously()
91 | }
92 | clients.updated(vo.connector.name, vo.connector.createClient)
93 | }
94 | case SetExtraResources(_) =>
95 | Repox.resourceHandlers.alter((for (er <- Config.resourceBases) yield {
96 | val resourceManager: FileResourceManager = new FileResourceManager(Paths.get(er).toFile, 100 * 1024)
97 | val resourceHandler = Handlers.resource(resourceManager)
98 | resourceManager -> resourceHandler
99 | }).toMap)
100 | Future {
101 | Map.empty[String, AsyncHttpClient]
102 | }
103 | case ImportConfig(_) =>
104 | for(client <- Repox.clients.get.valuesIterator) {
105 | client.closeAsynchronously()
106 | }
107 | for(manager <- Repox.resourceHandlers.get.keysIterator) {
108 | manager.close()
109 | }
110 | val fut1 = Repox.initClients()
111 | val fut2 = Repox.initResourceManagers()
112 | for (both <- fut1 zip fut2) yield {
113 | log.debug(s"ResourceBases (${Repox.resourceHandlers.get().keys.map(_.getBase).mkString(",")}) initialized.")
114 | log.debug(s"AHC clients (${Repox.clients.get().keys.mkString(",")}) initialized.")
115 | Map.empty[String, AsyncHttpClient]
116 | }
117 | case _ => Future {
118 | Map.empty[String, AsyncHttpClient]
119 | }
120 | }
121 | } {
122 | sender ! StatusCodes.OK
123 | }
124 | }
125 |
126 | val receiveCommand: Receive = {
127 | case cmd: ConfigCmd =>
128 | val newConfig = cmd.transform(config)
129 | if (newConfig == config) {
130 | // no change
131 | sender ! StatusCodes.OK
132 | } else {
133 | persist(ConfigChanged(newConfig, cmd))(onConfigSaved(sender(), _))
134 | }
135 | case UseDefault =>
136 | persist(UseDefault) { _ =>
137 | config = Config.default
138 | Config.set(config).foreach { _ =>
139 | Repox.requestQueueMaster ! RequestQueueMaster.ConfigLoaded
140 | }
141 | }
142 | case SaveSnapshot =>
143 | saveSnapshot(config)
144 | saveSnapshotRequester = Some(sender())
145 | case SaveSnapshotSuccess(metadata) =>
146 | log.debug(s"Config snapshot saved. Delete old ones.")
147 | deleteSnapshots(SnapshotSelectionCriteria(maxSequenceNr = metadata.sequenceNr - 1))
148 | case f@SaveSnapshotFailure(metadata, cause) =>
149 | log.debug(f.toString)
150 | for (requester <- saveSnapshotRequester) {
151 | requester ! Status.Failure(cause)
152 | saveSnapshotRequester = None
153 | }
154 | case DeleteSnapshotsSuccess(criteria) =>
155 | for (requester <- saveSnapshotRequester) {
156 | requester ! criteria.maxTimestamp
157 | saveSnapshotRequester = None
158 | }
159 | case DeleteSnapshotsFailure(criteria, cause) =>
160 | for (requester <- saveSnapshotRequester) {
161 | requester ! Status.Failure(cause)
162 | saveSnapshotRequester = None
163 | }
164 | }
165 |
166 | val receiveRecover: Receive = {
167 | case ConfigChanged(data, cmd) =>
168 | log.debug(s"Config changed, cmd=$cmd")
169 | config = data
170 |
171 | case UseDefault =>
172 | config = Config.default
173 |
174 | case SnapshotOffer(metadata, offeredSnapshot) =>
175 | config = offeredSnapshot.asInstanceOf[Config]
176 |
177 | case RecoveryCompleted =>
178 | if (config == null) {
179 | // no config history, save default data as snapshot
180 | self ! UseDefault
181 | } else {
182 | Config.set(config).foreach { _ =>
183 | Repox.requestQueueMaster ! RequestQueueMaster.ConfigLoaded
184 | }
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ConfigQuery.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import akka.actor.ActorSystem
4 | import akka.persistence.query.{EventEnvelope, PersistenceQuery}
5 | import akka.persistence.query.journal.leveldb.scaladsl.LeveldbReadJournal
6 | import akka.stream.ActorMaterializer
7 | import akka.stream.scaladsl.Source
8 | import com.typesafe.scalalogging.LazyLogging
9 |
10 | class ConfigQuery(val system: ActorSystem) extends LazyLogging {
11 | implicit val mat = ActorMaterializer()(system)
12 |
13 | val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
14 | LeveldbReadJournal.Identifier)
15 |
16 | val src =
17 | queries.eventsByPersistenceId("Config", 0L, Long.MaxValue)
18 |
19 | val events = src.map(_.event)
20 | events.runForeach {
21 | case ConfigChanged(_, cmd) => logger.debug(s"ConfigView received event caused by cmd: $cmd")
22 | case UseDefault => logger.debug(s"ConfigView received UseDefault evt")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ConnectorPersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.SerializationSupport
4 | import com.gtan.repox.admin.{ConnectorVO, RepoVO}
5 | import com.gtan.repox.data.{Connector, Repo}
6 | import play.api.libs.json.{JsValue, Json}
7 |
8 | object ConnectorPersister extends SerializationSupport {
9 |
10 | case class NewConnector(vo: ConnectorVO) extends ConfigCmd {
11 | override def transform(old: Config) = {
12 | val oldConnectors = old.connectors
13 | val oldProxyUsage = old.proxyUsage
14 | // ToDo: validation
15 | val voWithId = vo.copy(connector = vo.connector.copy(id = Some(Connector.nextId.incrementAndGet())))
16 | val newConfig = old.copy(connectors = oldConnectors + voWithId.connector)
17 | vo.proxy match {
18 | case None => newConfig
19 | case Some(p) => newConfig.copy(proxyUsage = oldProxyUsage.updated(voWithId.connector, p))
20 | }
21 | }
22 | }
23 |
24 | implicit val NewConnectorFormat = Json.format[NewConnector]
25 |
26 |
27 | case class UpdateConnector(vo: ConnectorVO) extends ConfigCmd {
28 | override def transform(old: Config) = {
29 | val oldConnectors = old.connectors
30 | val oldProxyUsage = old.proxyUsage
31 | val id = vo.connector.id
32 | val newConfig = old.copy(
33 | connectors = oldConnectors.map {
34 | case Connector(`id`, _, _, _, _, _, _) => vo.connector
35 | case c => c
36 | },
37 | connectorUsage = old.connectorUsage.map {
38 | case (repo, Connector(`id`, _, _, _, _, _, _)) => (repo, vo.connector)
39 | case p => p
40 | }
41 | )
42 | vo.proxy.fold(newConfig) { p =>
43 | newConfig.copy(proxyUsage = oldProxyUsage.updated(vo.connector, p))
44 | }
45 | }
46 | }
47 |
48 | implicit val updateConnectorFormat = Json.format[UpdateConnector]
49 |
50 | case class DeleteConnector(id: Long) extends ConfigCmd {
51 | override def transform(old: Config) = {
52 | val oldConnectors = old.connectors
53 | val oldConnectorUsage = old.connectorUsage
54 | old.copy(
55 | connectors = oldConnectors.filterNot(_.id.contains(id)),
56 | connectorUsage = oldConnectorUsage.filterNot { case (repo, connector) => repo.id.contains(id) }
57 | )
58 | }
59 | }
60 |
61 | implicit val DeleteConnectorFormat = Json.format[DeleteConnector]
62 |
63 | val NewConnectorClass = classOf[NewConnector].getName
64 | val UpdateConnectorClass = classOf[UpdateConnector].getName
65 | val DeleteConnectorClass = classOf[DeleteConnector].getName
66 |
67 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
68 | case NewConnectorClass => payload.as[NewConnector]
69 | case UpdateConnectorClass => payload.as[UpdateConnector]
70 | case DeleteConnectorClass => payload.as[DeleteConnector]
71 | }
72 | override val writer: PartialFunction[Jsonable, JsValue] = {
73 | case o: NewConnector => Json.toJson(o)
74 | case o: UpdateConnector => Json.toJson(o)
75 | case o: DeleteConnector => Json.toJson(o)
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ExpireRulePersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import javax.sql.rowset.serial.SerialStruct
4 |
5 | import com.gtan.repox.SerializationSupport
6 | import com.gtan.repox.data.ExpireRule
7 | import play.api.libs.json.{JsValue, Json}
8 |
9 | object ExpireRulePersister extends SerializationSupport {
10 |
11 | case class NewOrUpdateExpireRule(rule: ExpireRule) extends ConfigCmd {
12 | override def transform(old: Config) = {
13 | val oldRules = old.expireRules
14 | old.copy(expireRules = rule.id.fold(oldRules :+ rule.copy(id = Some(ExpireRule.nextId.incrementAndGet()))) { _id =>
15 | oldRules.map {
16 | case r@ExpireRule(Some(`_id`), _, _, _) => rule
17 | case r => r
18 | }
19 | })
20 | }
21 | }
22 |
23 | implicit val NewOrUpdateExpireRuleFormat = Json.format[NewOrUpdateExpireRule]
24 |
25 | case class EnableExpireRule(id: Long) extends ConfigCmd {
26 | override def transform(old: Config) = {
27 | val oldRules = old.expireRules
28 | old.copy(expireRules = oldRules.map {
29 | case p@ExpireRule(Some(`id`), _, _, _) => p.copy(disabled = false)
30 | case p => p
31 | })
32 | }
33 | }
34 |
35 | implicit val EnableExpireRuleFormat = Json.format[EnableExpireRule]
36 |
37 | case class DisableExpireRule(id: Long) extends ConfigCmd {
38 | override def transform(old: Config) = {
39 | val oldRules = old.expireRules
40 | old.copy(expireRules = oldRules.map {
41 | case p@ExpireRule(Some(`id`), _, _, _) => p.copy(disabled = true)
42 | case p => p
43 | })
44 | }
45 | }
46 |
47 | implicit val DisalbExpireRuleFormat = Json.format[DisableExpireRule]
48 |
49 | case class DeleteExpireRule(id: Long) extends ConfigCmd {
50 | override def transform(old: Config) = {
51 | val oldRules = old.expireRules
52 | old.copy(
53 | expireRules = oldRules.filterNot(_.id.contains(id))
54 | )
55 | }
56 | }
57 |
58 | implicit val DeleteExpireRuleFormat = Json.format[DeleteExpireRule]
59 |
60 | val NewOrUpdateExpireRuleClass = classOf[NewOrUpdateExpireRule].getName
61 | val EnableExpireRuleClass = classOf[EnableExpireRule].getName
62 | val DisableExpireRuleClass = classOf[DisableExpireRule].getName
63 | val DeleteExpireRuleClass = classOf[DeleteExpireRule].getName
64 |
65 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
66 | case NewOrUpdateExpireRuleClass => payload.as[NewOrUpdateExpireRule]
67 | case EnableExpireRuleClass => payload.as[EnableExpireRule]
68 | case DisableExpireRuleClass => payload.as[DisableExpireRule]
69 | case DeleteExpireRuleClass => payload.as[DeleteExpireRule]
70 |
71 | }
72 | override val writer: PartialFunction[Jsonable, JsValue] = {
73 | case o: NewOrUpdateExpireRule => Json.toJson(o)
74 | case o: EnableExpireRule => Json.toJson(o)
75 | case o: DisableExpireRule => Json.toJson(o)
76 | case o: DeleteExpireRule => Json.toJson(o)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/Immediate404RulePersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.SerializationSupport
4 | import com.gtan.repox.data.Immediate404Rule
5 | import play.api.libs.json.{JsValue, Json}
6 |
7 | object Immediate404RulePersister extends SerializationSupport {
8 |
9 | case class NewOrUpdateImmediate404Rule(rule: Immediate404Rule) extends ConfigCmd {
10 | override def transform(old: Config) = {
11 | val oldRules = old.immediate404Rules
12 | old.copy(immediate404Rules = rule.id.fold(oldRules :+ rule.copy(id = Some(Immediate404Rule.nextId.incrementAndGet()))) { _id =>
13 | oldRules.map {
14 | case r@Immediate404Rule(Some(`_id`), _, _, _) => rule
15 | case r => r
16 | }
17 | })
18 | }
19 | }
20 |
21 | implicit val newOrUpdateImmediate404RuleFormat = Json.format[NewOrUpdateImmediate404Rule]
22 |
23 | case class EnableImmediate404Rule(id: Long) extends ConfigCmd {
24 | override def transform(old: Config) = {
25 | val oldRules = old.immediate404Rules
26 | old.copy(immediate404Rules = oldRules.map {
27 | case p@Immediate404Rule(Some(`id`), _, _, _) => p.copy(disabled = false)
28 | case p => p
29 | })
30 | }
31 | }
32 |
33 | implicit val enableImmediate404RuleFormat = Json.format[EnableImmediate404Rule]
34 |
35 | case class DisableImmediate404Rule(id: Long) extends ConfigCmd {
36 | override def transform(old: Config) = {
37 | val oldRules = old.immediate404Rules
38 | old.copy(immediate404Rules = oldRules.map {
39 | case p@Immediate404Rule(Some(`id`), _, _, _) => p.copy(disabled = true)
40 | case p => p
41 | })
42 | }
43 | }
44 |
45 | implicit val DisableImmediate404RuleFormat = Json.format[DisableImmediate404Rule]
46 |
47 | case class DeleteImmediate404Rule(id: Long) extends ConfigCmd {
48 | override def transform(old: Config) = {
49 | val oldRules = old.immediate404Rules
50 | old.copy(
51 | immediate404Rules = oldRules.filterNot(_.id.contains(id))
52 | )
53 | }
54 | }
55 |
56 | implicit val DeleteImmediat404RuleFormat = Json.format[DeleteImmediate404Rule]
57 |
58 | val NewOrUpdateImmediate404RuleClass = classOf[NewOrUpdateImmediate404Rule].getName
59 | val EnableImmediate404RuleClass = classOf[EnableImmediate404Rule].getName
60 | val DisableImmediate404RuleClass = classOf[DisableImmediate404Rule].getName
61 | val DeleteImmediate404RuleClass = classOf[DeleteImmediate404Rule].getName
62 |
63 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
64 | case NewOrUpdateImmediate404RuleClass => payload.as[NewOrUpdateImmediate404Rule]
65 | case EnableImmediate404RuleClass => payload.as[EnableImmediate404Rule]
66 | case DisableImmediate404RuleClass => payload.as[DisableImmediate404Rule]
67 | case DeleteImmediate404RuleClass => payload.as[DeleteImmediate404Rule]
68 | }
69 | override val writer: PartialFunction[Jsonable, JsValue] = {
70 | case o: NewOrUpdateImmediate404Rule => Json.toJson(o)
71 | case o: EnableImmediate404Rule => Json.toJson(o)
72 | case o: DisableImmediate404Rule => Json.toJson(o)
73 | case o: DeleteImmediate404Rule => Json.toJson(o)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ParameterPersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.SerializationSupport
4 | import com.gtan.repox.data.DurationFormat
5 | import play.api.libs.json.{JsValue, Json}
6 |
7 | import scala.concurrent.duration.Duration
8 |
9 |
10 | object ParameterPersister extends SerializationSupport {
11 |
12 | case class SetHeadTimeout(m: Duration) extends ConfigCmd {
13 | override def transform(old: Config) = {
14 | old.copy(headTimeout = m)
15 | }
16 | }
17 |
18 |
19 | import DurationFormat._
20 | implicit val SetHeadTimeoutFormat = Json.format[SetHeadTimeout]
21 |
22 | case class SetHeadRetryTimes(m: Int) extends ConfigCmd {
23 | override def transform(old: Config) = {
24 | old.copy(headRetryTimes = m)
25 | }
26 | }
27 |
28 | implicit val SetHeadRetryTimesFormat = Json.format[SetHeadRetryTimes]
29 |
30 | case class ModifyPassword(newPassword: String) extends ConfigCmd {
31 | override def transform(old: Config): Config = {
32 | old.copy(password = newPassword)
33 | }
34 | }
35 |
36 | implicit val ModifyPasswordFormat = Json.format[ModifyPassword]
37 |
38 | case class SetExtraResources(value: String) extends ConfigCmd {
39 | override def transform(old: Config): Config = {
40 | old.copy(extraResources = value.split(":"))
41 | }
42 | }
43 |
44 | implicit val SetExtraResourcesFormat = Json.format[SetExtraResources]
45 |
46 | val SetHeadTimeoutClass = classOf[SetHeadTimeout].getName
47 | val SetHeadRetryTimesClass = classOf[SetHeadRetryTimes].getName
48 | val ModifyPasswordClass = classOf[ModifyPassword].getName
49 | val SetExtraResourcesClass = classOf[SetExtraResources].getName
50 |
51 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
52 | case SetHeadTimeoutClass => payload.as[SetHeadTimeout]
53 | case SetHeadRetryTimesClass => payload.as[SetHeadRetryTimes]
54 | case ModifyPasswordClass => payload.as[ModifyPassword]
55 | case SetExtraResourcesClass => payload.as[SetExtraResources]
56 | }
57 | override val writer: PartialFunction[Jsonable, JsValue] = {
58 | case o: SetHeadTimeout => Json.toJson(o)
59 | case o: SetHeadRetryTimes => Json.toJson(o)
60 | case o: ModifyPassword => Json.toJson(o)
61 | case o: SetExtraResources => Json.toJson(o)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/ProxyPersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.SerializationSupport
4 | import com.gtan.repox.data.{Connector, ProxyServer, Repo}
5 | import play.api.libs.json.{JsValue, Json}
6 |
7 | object ProxyPersister extends SerializationSupport {
8 |
9 | case class NewOrUpdateProxy(proxy: ProxyServer) extends ConfigCmd {
10 | override def transform(old: Config) = {
11 | val oldProxies = old.proxies
12 | val oldProxyUsages: Map[Connector, ProxyServer] = old.proxyUsage
13 | old.copy(proxies = proxy.id.fold(oldProxies :+ proxy.copy(id = Some(ProxyServer.nextId.incrementAndGet()))) { _id =>
14 | oldProxies.map {
15 | case ProxyServer(Some(`_id`), _, _, _, _, _) => proxy
16 | case p => p
17 | }
18 | }, proxyUsage = proxy.id.fold(oldProxyUsages) { _id =>
19 | oldProxyUsages.map {
20 | case (connector, ProxyServer(Some(`_id`), _, _, _, _, _)) => connector -> proxy
21 | case u => u
22 | }
23 | })
24 | }
25 | }
26 |
27 | implicit val newOrUpdateProxyformat = Json.format[NewOrUpdateProxy]
28 |
29 | case class EnableProxy(id: Long) extends ConfigCmd {
30 | override def transform(old: Config) = {
31 | old.copy(proxies = old.proxies.map {
32 | case p@ProxyServer(Some(`id`), _, _, _, _, _) => p.copy(disabled = false)
33 | case p => p
34 | })
35 | }
36 | }
37 |
38 | implicit val enableProxyFormat = Json.format[EnableProxy]
39 |
40 | case class DisableProxy(id: Long) extends ConfigCmd {
41 | override def transform(old: Config) = {
42 | old.copy(proxies = old.proxies.map {
43 | case p@ProxyServer(Some(`id`), _, _, _, _, _) => p.copy(disabled = true)
44 | case p => p
45 | }, proxyUsage = old.proxyUsage.filterNot {
46 | case (connector, proxy) => proxy.id.contains(id)
47 | })
48 | }
49 | }
50 |
51 | implicit val disableProxyFormat = Json.format[DisableProxy]
52 |
53 | case class DeleteProxy(id: Long) extends ConfigCmd {
54 | override def transform(old: Config) = {
55 | old.copy(
56 | proxies = old.proxies.filterNot(_.id.contains(id)),
57 | proxyUsage = old.proxyUsage.filterNot { case (connector, proxy) => proxy.id.contains(id) }
58 | )
59 | }
60 | }
61 |
62 | implicit val deleteProxyFormat = Json.format[DeleteProxy]
63 |
64 | val NewOrUpdateProxyClass = classOf[NewOrUpdateProxy].getName
65 | val EnableProxyClass = classOf[EnableProxy].getName
66 | val DisableProxyClass = classOf[DisableProxy].getName
67 | val DeleteProxyClass = classOf[DeleteProxy].getName
68 |
69 | override val reader: (JsValue) => PartialFunction[String, Jsonable] = payload => {
70 | case NewOrUpdateProxyClass => payload.as[NewOrUpdateProxy]
71 | case DisableProxyClass => payload.as[DisableProxy]
72 | case EnableProxyClass => payload.as[EnableProxy]
73 | case DeleteProxyClass => payload.as[DeleteProxy]
74 | }
75 |
76 | override val writer: PartialFunction[Jsonable, JsValue] = {
77 | case o: NewOrUpdateProxy => Json.toJson(o)
78 | case o: DisableProxy => Json.toJson(o)
79 | case o: EnableProxy => Json.toJson(o)
80 | case o: DeleteProxy => Json.toJson(o)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/config/RepoPersister.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.config
2 |
3 | import com.gtan.repox.SerializationSupport
4 | import com.gtan.repox.admin.RepoVO
5 | import com.gtan.repox.data.Repo
6 | import play.api.libs.json.{JsValue, Json}
7 |
8 | object RepoPersister extends SerializationSupport {
9 |
10 | case class NewRepo(vo: RepoVO) extends ConfigCmd {
11 | override def transform(old: Config) = {
12 | val oldRepos = old.repos
13 | val oldConnectorUsage = old.connectorUsage
14 | // ToDo: validation
15 | val voWithId = vo.copy(repo = vo.repo.copy(id = Some(Repo.nextId.incrementAndGet())))
16 | val insertPoint = oldRepos.indexWhere(_.priority > vo.repo.priority)
17 | val newRepos = if (insertPoint == -1) {
18 | // put to the last
19 | old.copy(repos = oldRepos :+ voWithId.repo)
20 | } else {
21 | val (before, after) = oldRepos.splitAt(insertPoint)
22 | old.copy(repos = (before :+ voWithId.repo) ++ after)
23 | }
24 | vo.connector match {
25 | case None => newRepos
26 | case Some(p) => newRepos.copy(connectorUsage = oldConnectorUsage.updated(voWithId.repo, p))
27 | }
28 | }
29 | }
30 |
31 | implicit val newRepoFormat = Json.format[NewRepo]
32 |
33 | case class DisableRepo(id: Long) extends ConfigCmd {
34 | override def transform(old: Config) = {
35 | val oldRepos = old.repos
36 | val oldConnectorUsage = old.connectorUsage
37 | old.copy(repos = oldRepos.map {
38 | case o@Repo(Some(`id`), _, _, _, _, _, _, _) => o.copy(disabled = true)
39 | case o => o
40 | }, connectorUsage = oldConnectorUsage.map {
41 | case (o@Repo(Some(`id`), _, _, _, _, _, _, _), connector) => o.copy(disabled = true) -> connector
42 | case u => u
43 | })
44 | }
45 | }
46 |
47 | implicit val disableRepoFormat = Json.format[DisableRepo]
48 |
49 | case class EnableRepo(id: Long) extends ConfigCmd {
50 | override def transform(old: Config) = {
51 | val oldRepos = old.repos
52 | val oldConnectorUsage = old.connectorUsage
53 | old.copy(repos = oldRepos.map {
54 | case o@Repo(Some(`id`), _, _, _, _, _, _, _) => o.copy(disabled = false)
55 | case o => o
56 | }, connectorUsage = oldConnectorUsage.map {
57 | case (o@Repo(Some(`id`), _, _, _, _, _, _, _), connector) => o.copy(disabled = false) -> connector
58 | case u => u
59 | })
60 | }
61 | }
62 |
63 | implicit val enableRepoFormat = Json.format[EnableRepo]
64 |
65 | case class DeleteRepo(id: Long) extends ConfigCmd {
66 | override def transform(old: Config) = {
67 | val oldRepos = old.repos
68 | val oldProxyUsage = old.connectorUsage
69 | old.copy(
70 | repos = oldRepos.filterNot(_.id.contains(id)),
71 | connectorUsage = oldProxyUsage.filterNot { case (repo, proxy) => repo.id.contains(id) }
72 | )
73 | }
74 | }
75 |
76 | implicit val deleteRepoFormat = Json.format[DeleteRepo]
77 |
78 | case class UpdateRepo(vo: RepoVO) extends ConfigCmd {
79 | override def transform(old: Config) = {
80 | val oldRepos = old.repos
81 | val oldConnectorUsage = old.connectorUsage
82 |
83 | val newConfig = for (found <- oldRepos.find(_.id == vo.repo.id)) yield {
84 | val indexOfTarget = oldRepos.indexOf(found)
85 | val repoUpdated: Config = old.copy(repos = oldRepos.updated(indexOfTarget, vo.repo))
86 | (oldConnectorUsage.get(vo.repo), vo.connector) match {
87 | case (None, None) => repoUpdated
88 | case (None, Some(p)) => repoUpdated.copy(connectorUsage = oldConnectorUsage.updated(vo.repo, p))
89 | case (Some(p), None) => repoUpdated.copy(connectorUsage = oldConnectorUsage - vo.repo)
90 | case (Some(o), Some(n)) if o == n => repoUpdated
91 | case (Some(o), Some(n)) => repoUpdated.copy(connectorUsage = oldConnectorUsage.updated(vo.repo, n))
92 | }
93 | }
94 | newConfig.getOrElse(old)
95 | }
96 | }
97 |
98 | implicit val updateRepoFormat = Json.format[UpdateRepo]
99 |
100 | case class MoveUpRepo(id: Long) extends ConfigCmd {
101 | override def transform(old: Config) = {
102 | val oldRepos = old.repos
103 | val oldConnectorUsage = old.connectorUsage
104 |
105 | val repo = oldRepos.find(_.id.contains(id))
106 | repo.fold(old) { _repo =>
107 | val index = oldRepos.indexOf(_repo)
108 | if (index == 0) {
109 | if (_repo.priority == 1) old // no higher level
110 | else old.copy(
111 | repos = oldRepos.map {
112 | case `_repo` => _repo.copy(priority = _repo.priority - 1)
113 | case r => r
114 | }, connectorUsage = oldConnectorUsage.map {
115 | case (o@Repo(Some(`id`), _, _, oldPriority, _, _, _, _), connector) => o.copy(priority = oldPriority - 1) -> connector
116 | case u => u
117 | })
118 | } else {
119 | val previous = oldRepos(index - 1)
120 | if (previous.priority == _repo.priority) {
121 | // swap this two
122 | old.copy(
123 | repos = oldRepos.map {
124 | case `previous` => _repo
125 | case `_repo` => previous
126 | case r => r
127 | }
128 | )
129 | } else {
130 | // if(previous.priority == _repo.priority - 1) uplevel as last
131 | // if(previous.priority < _repo.priority - 1) uplevel as the only one
132 | old.copy(
133 | repos = oldRepos.map {
134 | case `_repo` => _repo.copy(priority = _repo.priority - 1)
135 | case r => r
136 | }, connectorUsage = oldConnectorUsage.map {
137 | case (o@Repo(Some(`id`), _, _, oldPriority, _, _, _, _), connector) => o.copy(priority = oldPriority - 1) -> connector
138 | case u => u
139 | })
140 | }
141 | }
142 | }
143 | }
144 | }
145 |
146 | implicit val moveUpRepoFormat = Json.format[MoveUpRepo]
147 |
148 | case class MoveDownRepo(id: Long) extends ConfigCmd {
149 | override def transform(old: Config) = {
150 | val oldRepos = old.repos
151 | val repo = oldRepos.find(_.id.contains(id))
152 | repo.fold(old) { _repo =>
153 | val index = oldRepos.indexOf(_repo)
154 | if (index == oldRepos.length - 1) {
155 | if (_repo.priority == 10) old // no lower priority
156 | else old.copy(
157 | repos = oldRepos.map {
158 | case `_repo` => _repo.copy(priority = _repo.priority + 1)
159 | case r => r
160 | }
161 | )
162 | } else {
163 | val next = oldRepos(index + 1)
164 | if (next.priority == _repo.priority) {
165 | // swap this two
166 | old.copy(
167 | repos = oldRepos.map {
168 | case `next` => _repo
169 | case `_repo` => next
170 | case r => r
171 | }
172 | )
173 | } else {
174 | // if(next.priority == _repo.priority + 1) downlevel as first
175 | // if(next.priority > _repo.priority + 1) downlevel as the only one
176 | old.copy(
177 | repos = oldRepos.map {
178 | case `_repo` => _repo.copy(priority = _repo.priority + 1)
179 | case r => r
180 | }
181 | )
182 | }
183 | }
184 | }
185 | }
186 | }
187 |
188 | implicit val moveDownRepoFormat = Json.format[MoveDownRepo]
189 |
190 | val NewRepoClass = classOf[NewRepo].getName
191 | val DisableRepoClass = classOf[DisableRepo].getName
192 | val EnableRepoClass = classOf[EnableRepo].getName
193 | val DeleteRepoClass = classOf[DeleteRepo].getName
194 | val UpdateRepoClass = classOf[UpdateRepo].getName
195 | val MoveUpRepoClass = classOf[MoveUpRepo].getName
196 | val MoveDownRepoClass = classOf[MoveDownRepo].getName
197 |
198 | override val reader: JsValue => PartialFunction[String, Jsonable] = payload => {
199 | case NewRepoClass => payload.as[NewRepo]
200 | case DisableRepoClass => payload.as[DisableRepo]
201 | case EnableRepoClass => payload.as[EnableRepo]
202 | case DeleteRepoClass => payload.as[DeleteRepo]
203 | case UpdateRepoClass => payload.as[UpdateRepo]
204 | case MoveUpRepoClass => payload.as[MoveUpRepo]
205 | case MoveDownRepoClass => payload.as[MoveDownRepo]
206 | }
207 |
208 | override val writer: PartialFunction[Jsonable, JsValue] = {
209 | case o: NewRepo => Json.toJson(o)
210 | case o: DisableRepo => Json.toJson(o)
211 | case o: EnableRepo => Json.toJson(o)
212 | case o: DeleteRepo => Json.toJson(o)
213 | case o: UpdateRepo => Json.toJson(o)
214 | case o: MoveUpRepo => Json.toJson(o)
215 | case o: MoveDownRepo => Json.toJson(o)
216 | }
217 | }
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/Connector.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | import com.gtan.repox.config.Config
6 | import com.ning.http.client.Realm.{AuthScheme, RealmBuilder}
7 | import com.ning.http.client.{AsyncHttpClientConfig, AsyncHttpClient}
8 | import play.api.libs.json.Json
9 |
10 | import scala.concurrent.duration.Duration
11 | import com.ning.http.client.{Realm => JRealm}
12 |
13 | case class Realm(user: String, password: String, scheme: String) {
14 | def toJava: JRealm = new RealmBuilder().setPrincipal(user)
15 | .setPassword(password)
16 | .setUsePreemptiveAuth(true)
17 | .setScheme(AuthScheme.valueOf(scheme))
18 | .build()
19 | }
20 |
21 | case class Connector(id: Option[Long],
22 | name: String,
23 | connectionTimeout: Duration,
24 | connectionIdleTimeout: Duration,
25 | maxConnections: Int,
26 | maxConnectionsPerHost: Int,
27 | credentials: Option[Realm] = None) {
28 |
29 |
30 | def createClient = {
31 | val configBuilder = new AsyncHttpClientConfig.Builder()
32 | .setRequestTimeout(Int.MaxValue)
33 | .setReadTimeout(connectionIdleTimeout.toMillis.toInt)
34 | .setConnectTimeout(connectionTimeout.toMillis.toInt)
35 | .setPooledConnectionIdleTimeout(connectionIdleTimeout.toMillis.toInt)
36 | .setAllowPoolingConnections(true)
37 | .setAllowPoolingSslConnections(true)
38 | .setMaxConnectionsPerHost(maxConnections)
39 | .setMaxConnections(maxConnectionsPerHost)
40 | .setFollowRedirect(true)
41 | val withCredentials = this.credentials.fold(configBuilder) { x => configBuilder.setRealm(x.toJava) }
42 | val builder = Config.proxyUsage.get(this).fold(withCredentials) { x => withCredentials.setProxyServer(x.toJava) }
43 |
44 | new AsyncHttpClient(builder.build())
45 | }
46 | }
47 |
48 | object Connector {
49 | lazy val nextId: AtomicLong = new AtomicLong(Config.connectors.flatMap(_.id).reduceOption[Long](math.max).getOrElse(1L))
50 |
51 | import DurationFormat._
52 |
53 | implicit val realmFormat = Json.format[Realm]
54 | implicit val format = Json.format[Connector]
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/DurationFormat.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import play.api.libs.json._
4 |
5 | import scala.concurrent.duration.Duration
6 |
7 | object DurationFormat {
8 | implicit val durationFormat = new Format[Duration] {
9 | override def reads(json: JsValue) = json match {
10 | case JsString(str) =>
11 | JsSuccess(Duration(str))
12 | case _ =>
13 | JsError("duration json format need string")
14 | }
15 |
16 | override def writes(o: Duration) = JsString(o.toString)
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/ExpireRule.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import play.api.libs.json._
8 |
9 | import scala.concurrent.duration.Duration
10 |
11 | import collection.JavaConverters._
12 |
13 | case class ExpireRule(id: Option[Long], pattern: String, duration: Duration, disabled: Boolean = false)
14 |
15 | object ExpireRule {
16 |
17 | import DurationFormat._
18 |
19 | lazy val nextId: AtomicLong = new AtomicLong(Config.expireRules.flatMap(_.id).reduceOption[Long](math.max).getOrElse(1))
20 |
21 | implicit val format = Json.format[ExpireRule]
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/Immediate404Rule.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import play.api.libs.json.Json
8 |
9 | import scala.collection.JavaConverters._
10 |
11 | /**
12 | * Created by IntelliJ IDEA.
13 | * User: xf
14 | * Date: 14/11/23
15 | * Time: 下午12:15
16 | */
17 | case class Immediate404Rule(id: Option[Long], include: String, exclude: Option[String] = None, disabled: Boolean = false) {
18 | def matches(uri: String): Boolean = {
19 | val included = uri.matches(include)
20 | exclude match {
21 | case None => included
22 | case Some(regex) =>
23 | val excluded = uri.matches(regex)
24 | included && !excluded
25 | }
26 | }
27 |
28 | }
29 |
30 | object Immediate404Rule {
31 | lazy val nextId: AtomicLong = new AtomicLong(Config.immediate404Rules.flatMap(_.id).reduceOption[Long](math.max).getOrElse(1))
32 | implicit val format = Json.format[Immediate404Rule]
33 | }
34 |
35 | case class BlacklistRule(pattern: String, repoName: String)
36 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/ProxyServer.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | import com.gtan.repox.Repox
6 | import com.gtan.repox.config.Config
7 | import com.ning.http.client.ProxyServer.Protocol
8 | import com.ning.http.client.{ProxyServer => JProxyServer}
9 | import play.api.libs.json._
10 |
11 | import scala.collection.JavaConverters._
12 |
13 | case class ProxyServer(id: Option[Long], name: String, protocol: JProxyServer.Protocol, host: String, port: Int, disabled: Boolean = false) {
14 | def toJava: JProxyServer = new JProxyServer(protocol, host, port)
15 |
16 | }
17 |
18 | object ProxyServer {
19 | lazy val nextId: AtomicLong = new AtomicLong(Config.proxies.flatMap(_.id).reduceOption[Long](math.max).getOrElse(1))
20 |
21 | implicit val protocolFormat = new Format[JProxyServer.Protocol] {
22 | override def reads(json: JsValue):JsResult[JProxyServer.Protocol] = json match {
23 | case JsString(str) =>
24 | JsSuccess(JProxyServer.Protocol.valueOf(str))
25 | case _ =>
26 | JsError("not a valid protocol")
27 | }
28 |
29 | override def writes(o: Protocol) = JsString(o.name())
30 | }
31 |
32 | implicit val format = Json.format[ProxyServer]
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/scala/com/gtan/repox/data/Repo.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox.data
2 |
3 | import java.net.URL
4 | import java.util.concurrent.atomic.AtomicLong
5 |
6 | import com.gtan.repox.config.Config
7 | import play.api.libs.json.Json
8 |
9 | /**
10 | * Created by IntelliJ IDEA.
11 | * User: xf
12 | * Date: 14/11/23
13 | * Time: 下午3:55
14 | */
15 | case class Repo(id: Option[Long], name: String, base: String, priority: Int, getOnly: Boolean = false, maven: Boolean = false, disabled: Boolean = false, parentId: Option[Long] = None) {
16 | def absolute(uri: String): String = base + uri
17 | lazy val host = new URL(base).getHost
18 | }
19 |
20 | object Repo {
21 | lazy val nextId: AtomicLong = new AtomicLong(Config.repos.flatMap(_.id).reduceOption[Long](math.max).getOrElse(1))
22 |
23 | implicit val format = Json.format[Repo]
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/test/scala/com/gtan/repox/GetMasterSuite.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import java.net.URLEncoder
4 |
5 | import akka.actor.ActorPath
6 | import org.scalatest._
7 |
8 | class GetMasterSuite extends FunSuite{
9 | val src = Seq("ascii", "中文Path", "带 空 格", ".其$它*特@殊!字#符~:+")
10 | /**
11 | * URLEncoder is used to generate part of GetWorker/HeadWorker names
12 | */
13 | test("normalized actor paths are valid") {
14 | assert(src.forall{ path =>
15 | ActorPath.isValidPathElement(URLEncoder.encode(path, "UTF-8"))
16 | })
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/test/scala/com/gtan/repox/RepoxSuite.scala:
--------------------------------------------------------------------------------
1 | package com.gtan.repox
2 |
3 | import org.scalatest._
4 |
5 | class RepoxSuite extends FunSuite {
6 | val mavenData = Map(
7 | "/org/scala-lang/modules/scalajs/scalajs-sbt-plugin_2.10_0.13/0.5.6/scalajs-sbt-plugin-0.5.6.pom" -> "/org.scala-lang.modules.scalajs/scalajs-sbt-plugin/scala_2.10/sbt_0.13/0.5.6/ivys/ivy.xml",
8 | "/io/spray/sbt-revolver_2.10_0.13/0.7.2/sbt-revolver-0.7.2.pom" -> "/io.spray/sbt-revolver/scala_2.10/sbt_0.13/0.7.2/ivys/ivy.xml",
9 | "/org/scala-lang/modules/scalajs/scalajs-tools_2.10/0.5.6/scalajs-tools_2.10-0.5.6.pom" -> "/org.scala-lang.modules.scalajs/scalajs-tools_2.10/0.5.6/ivys/ivy.xml",
10 | "/com/eed3si9n/sbt-assembly_2.10_0.13/0.12.0/sbt-assembly-0.12.0.pom" -> "/com.eed3si9n/sbt-assembly/scala_2.10/sbt_0.13/0.12.0/ivys/ivy.xml",
11 | "/org/scala-sbt/sbt/0.13.7/sbt-0.13.7.pom" -> "/org.scala-sbt/sbt/0.13.7/ivys/ivy.xml",
12 | "/org/scala-sbt/main/0.13.7/main-0.13.7.pom" -> "/org.scala-sbt/main/0.13.7/ivys/ivy.xml",
13 | "/org/scala-sbt/actions/0.13.7/actions-0.13.7.pom" -> "/org.scala-sbt/actions/0.13.7/ivys/ivy.xml"
14 | , "/net/virtual-void/sbt-dependency-graph/0.7.4/sbt-dependency-graph-0.7.4.jar" -> "/net/virtual-void/sbt-dependency-graph_2.10_0.13/0.7.4/sbt-dependency-graph-0.7.4.jar"
15 | , "/com/github/mpeltonen/sbt-idea/1.6.0/sbt-idea-1.6.0.jar" -> "/com/github/mpeltonen/sbt-idea_2.10_0.13/1.6.0/sbt-idea-1.6.0.jar"
16 | )
17 |
18 | val ivyData = Map(
19 | "/net.virtual-void/sbt-dependency-graph/0.7.4/jars/sbt-dependency-graph.jar" -> "/net/virtual-void/sbt-dependency-graph_2.10_0.13/0.7.4/sbt-dependency-graph-0.7.4.jar"
20 | , "/com.github.mpeltonen/sbt-idea/1.6.0/jars/sbt-idea.jar" -> "/com/github/mpeltonen/sbt-idea_2.10_0.13/1.6.0/sbt-idea-1.6.0.jar"
21 | )
22 |
23 | val invalidData = Seq(
24 | "/joda-time/joda-time/",
25 | "/joda-time/joda-time"
26 | )
27 |
28 | test("MavenFormat should match keys of mavenData") {
29 | assert(mavenData.keys.forall(_.matches(Repox.MavenFormat.regex)))
30 | }
31 |
32 | test("IvyFormat should match keys of ivyData") {
33 | assert(ivyData.keys.forall(_.matches(Repox.IvyFormat.regex)))
34 | }
35 |
36 | test("keys in all data map should have the value as peer") {
37 | assert((mavenData ++ ivyData).forall { case (k, v) =>
38 | val peers = Repox.peer(k)
39 | peers.isSuccess && peers.get.contains(v)
40 | })
41 | }
42 |
43 | test("requests in invalidData are invalid request") {
44 | assert(invalidData.forall { uri =>
45 | Repox.peer(uri).isFailure
46 | })
47 | }
48 | }
49 |
--------------------------------------------------------------------------------