Support for placeholders in git and file URIs

The default git repository and also the native one (local files)
now support placeholders for {application}, {profile} and
{label} in the URI (searchLocation for the native repository).

Fixes gh-257
This commit is contained in:
Dave Syer
2015-12-02 11:14:45 +00:00
parent ccdbfb04e6
commit 87a1e773a0
32 changed files with 418 additions and 31 deletions

View File

@@ -142,11 +142,33 @@ avoid ambiguity with other URL paths). Be careful with the brackets in
the URL if you are using a command line client like curl (e.g. escape
them from the shell with quotes '').
Spring Cloud Config Server supports a single or multiple git
repositories with pattern matching on the application and profile
name. The pattern format is a comma-separated list of
`{application}/{profile}` names with wildcards (where a pattern
beginning with a wildcard may need to be quoted). Example:
===== Placeholders in Git URI
Spring Cloud Config Server supports a git repository URL with
placeholders for the `{application}` and `{profile}` (and `{label}` if
you need it, but remember that the label is applied as a git label
anyway). So you can easily support a "one repo per application" policy
using (for example):
----
spring:
cloud:
config:
server:
git:
uri: https://github.com/myorg/{application}
----
or a "one repo per profile" policy using a similar pattern but with
`{profile}`.
===== Pattern Matching and Multiple Repositories
There is also support for more complex requirements with pattern
matching on the application and profile name. The pattern format is a
comma-separated list of `{application}/{profile}` names with wildcards
(where a pattern beginning with a wildcard may need to be
quoted). Example:
----
spring:
@@ -279,7 +301,9 @@ of the box when you store keys in the default directories (`~/.ssh`)
and the uri points to an SSH location,
e.g. "git@github.com:configuration/cloud-configuration". The
repository is accessed using JGit, so any documentation you find on
that should be applicable.
that should be applicable. HTTPS proxy settings can be set in
`~/.git/config` or in the same way as for any other JVM process via
system properties (`-Dhttps.proxyHost` and `-Dhttps.proxyPort`).
==== File System Backend
@@ -291,20 +315,83 @@ profile just launch the Config Server with
"spring.profiles.active=native".
WARNING: The default value of the `searchLocations` is identical to a
local Spring Boot application (so
`[classpath:/, classpath:/config, file:./, file:./config]`) which will
expose the `application.properties` from the server to all clients.
local Spring Boot application (so `[classpath:/, classpath:/config,
file:./, file:./config]`). This does not expose the
`application.properties` from the server to all clients because any
property sources present in the server are removed before being sent
to the client.
TIP: A filesystem backend is great for getting started quickly and
for testing. To use it in production you need to be sure that the
file system is reliable, and shared across all instances of the
Config Server.
This repository implementation maps the `{label}` parameter of the
HTTP resource to a suffix on the search path, so properties files are
loaded from each search location *and* a subdirectory with the same
name as the label (the labelled properties take precedence in the
Spring Environment).
The search locations can contain placeholders for `{application}`,
`{profile}` and `{label}`. In this way you can segregate the
directories in the path, and choose a strategy that makes sense for
you (e.g. sub-directory per application, or sub-directory per
profile).
If you don't use placeholders in the search locations, this repository
also appends the `{label}` parameter of the HTTP resource to a suffix
on the search path, so properties files are loaded from each search
location *and* a subdirectory with the same name as the label (the
labelled properties take precedence in the Spring Environment). Thus
the default behaviour with no placeholders is the same as adding a
search location ending with `/{label}/. For example `file:/tmp/config`
is the same as `file:/tmp/config,file:/tmp/config/{label}`
==== Sharing Configiration With All Applications
With file-based (i.e. git, svn and native) repositories, resources
with file names in `application*` are shared between all client
applications (so `application.properties`, `application.yml`,
`application-*.properties` etc.). You can use resources with these
file names to configure global defaults and have them overridden by
application-specific files as necessary.
The #_property_overrides[property overrides] feature can also be used
for setting global defaults, and with placeholders applications are
allowed to override them locally.
TIP: With the "native" profile (local file system backend) it is
recommended that you use an explicit search location that isn't part
of the server's own configuration. Otherwise the `application*`
resources in the default search locations are removed because they are
part of the server.
==== Property Overrides
The Config Server has an "overrides" feature that allows the operator
to provide configuration properties to all applications that cannot be
accidentally changed by the application using the normal Spring Boot
hooks. To declare overrides just add a map of name-value pairs to
`spring.cloud.config.server.overrides`. For example
----
spring:
cloud:
config:
server:
foo: bar
----
will cause all applications that are config clients to read `foo=bar`
independent of their own configuration. (Of course an application can
use the data in the Config Server in any way it likes, so overrides
are not enforceable, but they do provide useful default behaviour if
they are Spring Cloud Config clients.)
TIP: Normal, Spring environment placeholders with "${}" can be escaped
(and resolved on the client) by using backslash ("\") to escape the
"$", e.g. `\${app.foo:bar}` resolves to "bar" unless the app provides
its own "app.foo". Note that in YAML you don't need to escape the
backslash itself, but in properties files you do, when you configure
the overrides on the server.
You can change the priority of all overrides in the client to be more
like default values, allowing applications to supply their own values
in environment variables or System properties, by setting the flag `
=== Health Indicator

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.config.server.environment;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -23,6 +24,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.ConfigurableEnvironment;
@@ -48,6 +50,8 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
private Map<String, PatternMatchingJGitEnvironmentRepository> repos = new LinkedHashMap<String, PatternMatchingJGitEnvironmentRepository>();
private Map<String, JGitEnvironmentRepository> placeholders = new LinkedHashMap<String, JGitEnvironmentRepository>();
public MultipleJGitEnvironmentRepository(ConfigurableEnvironment environment) {
super(environment);
}
@@ -82,24 +86,72 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
@Override
public Locations getLocations(String application, String profile, String label) {
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
Environment source = repository.findOne(application, profile, label);
if (source != null) {
return repository.getLocations(application, profile, label);
if (repository.matches(application, profile, label)) {
JGitEnvironmentRepository candidate = getRepository(repository,
application, profile, label);
Environment source = candidate.findOne(application, profile, label);
if (source != null) {
return repository.getLocations(application, profile, label);
}
}
}
return super.getLocations(application, profile, label);
JGitEnvironmentRepository candidate = getRepository(this,
application, profile, label);
if (candidate==this) {
return super.getLocations(application, profile, label);
}
return candidate.getLocations(application, profile, label);
}
@Override
public Environment findOne(String application, String profile, String label) {
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
Environment source = repository.findOne(application, profile, label);
if (source != null) {
return source;
if (repository.matches(application, profile, label)) {
JGitEnvironmentRepository candidate = getRepository(repository,
application, profile, label);
Environment source = candidate.findOne(application, profile, label);
if (source != null) {
return source;
}
}
}
JGitEnvironmentRepository candidate = getRepository(this,
application, profile, label);
if (candidate==this) {
return super.findOne(application, profile, label);
}
return candidate.findOne(application, profile, label);
}
return super.findOne(application, profile, label);
private JGitEnvironmentRepository getRepository(JGitEnvironmentRepository repository,
String application, String profile, String label) {
if (!repository.getUri().contains("{")) {
return repository;
}
String key = repository.getUri();
if (application!=null) {
key = key.replace("{application}", application);
}
if (profile!=null) {
key = key.replace("{profile}", profile);
}
if (label!=null) {
key = key.replace("{label}", label);
}
if (!this.repos.containsKey(key)) {
this.placeholders.put(key, getRepository(repository, key));
}
return this.placeholders.get(key);
}
private JGitEnvironmentRepository getRepository(JGitEnvironmentRepository source,
String uri) {
JGitEnvironmentRepository repository = new JGitEnvironmentRepository(null);
File basedir = repository.getBasedir();
BeanUtils.copyProperties(source, repository);
repository.setUri(uri);
repository.setBasedir(basedir);
return repository;
}
public static class PatternMatchingJGitEnvironmentRepository
@@ -117,6 +169,18 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
setUri(uri);
}
public boolean matches(String application, String profile, String label) {
if (this.pattern == null || this.pattern.length == 0) {
return false;
}
if (PatternMatchUtils.simpleMatch(this.pattern,
application + "/" + profile)) {
return true;
}
return false;
}
@Override
public Environment findOne(String application, String profile, String label) {

View File

@@ -98,7 +98,7 @@ public class NativeEnvironmentRepository
ConfigurableEnvironment environment = getEnvironment(profile);
builder.environment(environment);
builder.web(false).bannerMode(Mode.OFF);
String[] args = getArgs(config, label);
String[] args = getArgs(config, profile, label);
// Explicitly set the listeners (to exclude logging listener which would change
// log levels in the caller)
builder.application()
@@ -122,7 +122,22 @@ public class NativeEnvironmentRepository
}
List<String> output = new ArrayList<String>();
for (String location : locations) {
output.add(location);
String value = location;
if (application != null) {
value = value.replace("{application}", application);
}
if (profile != null) {
value = value.replace("{profile}", profile);
}
if (label != null) {
value = value.replace("{label}", label);
}
if (!value.endsWith("/")) {
value = value + "/";
}
if (isDirectory(value)) {
output.add(value);
}
}
for (String location : locations) {
if (isDirectory(location) && StringUtils.hasText(label)) {
@@ -160,7 +175,8 @@ public class NativeEnvironmentRepository
.cleanPath(new File(normal.substring("file:".length()))
.getAbsolutePath());
}
for (String pattern : getLocations(null, null, result.getLabel())
String profile = result.getProfiles() == null ? null : StringUtils.arrayToCommaDelimitedString(result.getProfiles());
for (String pattern : getLocations(result.getName(), profile, result.getLabel())
.getLocations()) {
if (!pattern.contains(":")) {
pattern = "file:" + pattern;
@@ -195,8 +211,9 @@ public class NativeEnvironmentRepository
return result;
}
private String[] getArgs(String config, String label) {
private String[] getArgs(String application, String profile, String label) {
List<String> list = new ArrayList<String>();
String config = application;
if (!config.startsWith("application")) {
config = "application," + config;
}
@@ -204,7 +221,7 @@ public class NativeEnvironmentRepository
list.add("--spring.cloud.bootstrap.enabled=false");
list.add("--encrypt.failOnError=" + this.failOnError);
list.add("--spring.config.location=" + StringUtils.arrayToCommaDelimitedString(
getLocations(null, null, label).getLocations()));
getLocations(application, profile, label).getLocations()));
return list.toArray(new String[0]);
}
@@ -234,8 +251,8 @@ public class NativeEnvironmentRepository
}
private boolean isDirectory(String location) {
return !location.endsWith(".properties") && !location.endsWith(".yml")
&& !location.endsWith(".yaml");
return !location.contains("{") && !location.endsWith(".properties")
&& !location.endsWith(".yml") && !location.endsWith(".yaml");
}
}

View File

@@ -56,7 +56,7 @@ public class JGitEnvironmentRepositoryIntegrationTests {
if (this.basedir.exists()) {
FileUtils.delete(this.basedir, FileUtils.RECURSIVE);
}
ConfigServerTestUtils.deleteLocalRepo("config-copy");
ConfigServerTestUtils.deleteLocalRepo("");
}
@After

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.server.environment;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository.PatternMatchingJGitEnvironmentRepository;
import org.springframework.cloud.config.server.test.ConfigServerTestUtils;
import org.springframework.core.env.StandardEnvironment;
/**
* @author Dave Syer
*
*/
public class MultipleJGitEnvironmentUriTemplateRepositoryTests {
private StandardEnvironment environment = new StandardEnvironment();
private MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository(
this.environment);
@Before
public void init() throws Exception {
String defaultUri = ConfigServerTestUtils.prepareLocalRepo("config-repo");
this.repository.setUri(defaultUri);
this.repository.setRepos(createRepositories());
}
private Map<String, PatternMatchingJGitEnvironmentRepository> createRepositories()
throws Exception {
String test1Uri = ConfigServerTestUtils.prepareLocalRepo("test1-config-repo");
ConfigServerTestUtils.prepareLocalRepo("test2-config-repo");
Map<String, PatternMatchingJGitEnvironmentRepository> repos = new HashMap<>();
repos.put("templates", createRepository("test", "*-config-repo",
test1Uri.replace("test1-config-repo", "{application}")));
return repos;
}
private PatternMatchingJGitEnvironmentRepository createRepository(String name,
String pattern, String uri) {
PatternMatchingJGitEnvironmentRepository repo = new PatternMatchingJGitEnvironmentRepository();
repo.setEnvironment(this.environment);
repo.setName(name);
repo.setPattern(new String[] { pattern });
repo.setUri(uri);
return repo;
}
@Test
public void defaultRepo() {
Environment environment = this.repository.findOne("bar", "staging", "master");
assertEquals(2, environment.getPropertySources().size());
assertEquals(this.repository.getUri() + "/bar.properties",
environment.getPropertySources().get(0).getName());
assertVersion(environment);
}
@Test
public void mappingRepo() {
Environment environment = this.repository.findOne("test1-config-repo", "staging",
"master");
assertEquals(1, environment.getPropertySources().size());
assertEquals(
getUri("*").replace("{application}", "test1-config-repo")
+ "/application.yml",
environment.getPropertySources().get(0).getName());
assertVersion(environment);
}
@Test
public void otherMappingRepo() {
Environment environment = this.repository.findOne("test2-config-repo", "staging",
"master");
assertEquals(1, environment.getPropertySources().size());
assertEquals(
getUri("*").replace("{application}", "test2-config-repo")
+ "/application.properties",
environment.getPropertySources().get(0).getName());
assertVersion(environment);
}
private void assertVersion(Environment environment) {
String version = environment.getVersion();
assertNotNull("version was null", version);
assertTrue("version length was wrong",
version.length() >= 40 && version.length() <= 64);
}
private String getUri(String pattern) {
String uri = null;
Map<String, PatternMatchingJGitEnvironmentRepository> repoMappings = this.repository
.getRepos();
for (PatternMatchingJGitEnvironmentRepository repo : repoMappings.values()) {
String[] mappingPattern = repo.getPattern();
if (mappingPattern != null && mappingPattern.length != 0) {
uri = repo.getUri();
break;
}
}
return uri;
}
}

View File

@@ -21,6 +21,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.server.environment.SearchPathLocator.Locations;
import org.springframework.context.ConfigurableApplicationContext;
/**
@@ -43,7 +44,7 @@ public class NativeEnvironmentRepositoryTests {
@Test
public void emptySearchLocations() {
this.repository.setSearchLocations((String[])null);
this.repository.setSearchLocations((String[]) null);
Environment environment = this.repository.findOne("foo", "development", "master");
assertEquals(2, environment.getPropertySources().size());
}
@@ -91,4 +92,48 @@ public class NativeEnvironmentRepositoryTests {
assertEquals("version was wrong", "myversion", environment.getVersion());
}
@Test
public void placeholdersLabel() {
this.repository.setSearchLocations("classpath:/test/{label}/");
Environment environment = this.repository.findOne("foo", "development", "dev");
assertEquals(1, environment.getPropertySources().size());
assertEquals("dev_bar",
environment.getPropertySources().get(0).getSource().get("foo"));
}
@Test
public void placeholdersProfile() {
this.repository.setSearchLocations("classpath:/test/{profile}/");
Environment environment = this.repository.findOne("foo", "dev", "master");
assertEquals(1, environment.getPropertySources().size());
assertEquals("dev_bar",
environment.getPropertySources().get(0).getSource().get("foo"));
}
@Test
public void placeholdersApplicationAndProfile() {
this.repository.setSearchLocations("classpath:/test/{profile}/{application}/");
Environment environment = this.repository.findOne("app", "dev", "master");
assertEquals(1, environment.getPropertySources().size());
assertEquals("app",
environment.getPropertySources().get(0).getSource().get("foo"));
}
@Test
public void locationPlaceholdersApplication() {
this.repository.setSearchLocations("classpath:/test/{application}");
Locations locations = this.repository.getLocations("foo", "dev", "master");
assertEquals(1, locations.getLocations().length);
assertEquals("classpath:/test/foo/", locations.getLocations()[0]);
}
@Test
public void placeholdersNoTrailingSlash() {
this.repository.setSearchLocations("classpath:/test/{label}");
Environment environment = this.repository.findOne("foo", "development", "dev");
assertEquals(1, environment.getPropertySources().size());
assertEquals("dev_bar",
environment.getPropertySources().get(0).getSource().get("foo"));
}
}

View File

@@ -0,0 +1,8 @@
Add application.properties
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# new file: application.properties
# deleted: application.yml
#

View File

@@ -0,0 +1,2 @@
7df4a26d5437d9d4090cd5809967f870444cde8f not-for-merge branch 'raw' of dsyer@localhost:/home/dsyer/dev/platform/config/spring-platform-config-server/target/test-classes/config-repo
7df4a26d5437d9d4090cd5809967f870444cde8f not-for-merge tag 'foo' of dsyer@localhost:/home/dsyer/dev/platform/config/spring-platform-config-server/target/test-classes/config-repo

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1 @@
dfd1f2f006c9ea71be688e3eeaf383ff640fd21a

View File

@@ -0,0 +1,5 @@
[core]
repositoryformatversion = 0
filemode = true
logallrefupdates = true
[branch "raw"]

View File

@@ -0,0 +1,3 @@
1 1
7df4a26d5437d9d4090cd5809967f870444cde8f 9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0 9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0
1

View File

@@ -0,0 +1,8 @@
0000000000000000000000000000000000000000 7df4a26d5437d9d4090cd5809967f870444cde8f Dave Syer <dsyer@gopivotal.com> 1406860717 -0700
7df4a26d5437d9d4090cd5809967f870444cde8f 9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0 Dave Syer <dsyer@pivotal.io> 1415902155 +0000 checkout: moving from raw to master
9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0 c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 Dave Syer <dsyer@pivotal.io> 1415902338 +0000 commit: Move application.yml to subdir
c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 dfd1f2f006c9ea71be688e3eeaf383ff640fd21a Dave Syer <dsyer@pivotal.io> 1449047214 +0000 commit: Fix
dfd1f2f006c9ea71be688e3eeaf383ff640fd21a c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 Dave Syer <dsyer@pivotal.io> 1449047236 +0000 rebase -i (start): checkout 9f01f
c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 dfe21d586fc45ba72db173421c5139e26e50a7af Dave Syer <dsyer@pivotal.io> 1449047236 +0000 rebase -i (squash): Move application.yml to subdir
dfe21d586fc45ba72db173421c5139e26e50a7af dfe21d586fc45ba72db173421c5139e26e50a7af Dave Syer <dsyer@pivotal.io> 1449047243 +0000 rebase -i (finish): returning to refs/heads/master
dfe21d586fc45ba72db173421c5139e26e50a7af 29f2b1020f4b87e1de6aab368ff496cc4e99a959 Dave Syer <dsyer@pivotal.io> 1449047248 +0000 commit (amend): Add application.properties

View File

@@ -0,0 +1,5 @@
0000000000000000000000000000000000000000 9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0 Dave Syer <dsyer@gopivotal.com> 1406860776 -0700 branch: Created from 9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0
9f01fb972bc9617e4ea59f5c8ee3ceb5ff515cd0 c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 Dave Syer <dsyer@pivotal.io> 1415902338 +0000 commit: Move application.yml to subdir
c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4 dfd1f2f006c9ea71be688e3eeaf383ff640fd21a Dave Syer <dsyer@pivotal.io> 1449047214 +0000 commit: Fix
dfd1f2f006c9ea71be688e3eeaf383ff640fd21a dfe21d586fc45ba72db173421c5139e26e50a7af Dave Syer <dsyer@pivotal.io> 1449047243 +0000 rebase -i (finish): refs/heads/master onto c4bd92016dc14b9fe376d9c3f7af1c9d22d44ee4
dfe21d586fc45ba72db173421c5139e26e50a7af 29f2b1020f4b87e1de6aab368ff496cc4e99a959 Dave Syer <dsyer@pivotal.io> 1449047248 +0000 commit (amend): Add application.properties

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 7df4a26d5437d9d4090cd5809967f870444cde8f Dave Syer <dsyer@gopivotal.com> 1406860717 -0700

View File

@@ -0,0 +1 @@
x+)JMU01f040031QH,(<28><>LN,<2C><><EFBFBD>ӫ<EFBFBD><D3AB>ah<61>xH<78>S<EFBFBD>O<EFBFBD>}jI<6A><49><EFBFBD><EFBFBD><EFBFBD><EFBFBD><07> <01>x<12>

View File

@@ -0,0 +1 @@
x<01><>Kj1D<><44>)z0<><30>g<1B>=<3D>n,<2C>-!ˆ<>>"<22><>kSP<53><07><>z-<03><><EFBFBD><EFBFBD>E@jR<6A><52>#<23><>d<EFBFBD><64><13>I<EFBFBD><49><EFBFBD><EFBFBD>U<EFBFBD><55><EFBFBD>`u<>

View File

@@ -0,0 +1,2 @@
x<01><>1
1E<>s<EFBFBD><73><05>$<24><>

View File

@@ -0,0 +1,2 @@
x<01><>K
1D]<5D><14> <0B>O;<3B><17><A<>Ӎ<01> 1z{<7B>7<EFBFBD>6<0F>WPT<50>97<39≯i<CCB8>Y<EFBFBD>3<EFBFBD>bM<62><4D>"<22><><EFBFBD>{%<25>1<EFBFBD>@"&<26>cPK<50><4B>h<EFBFBD>`J<><4A>Y<>P؇!!y Q,ar.p<><70><EFBFBD><EFBFBD>J<EFBFBD><EFBFBD>~<7E><>Czv<7A><76><EFBFBD><EFBFBD><16><>\<5C><><02><><EFBFBD>,<2C><><EFBFBD>Q<EFBFBD><><D7BA>N]<5D>[}<01><>A<EFBFBD>

View File

@@ -0,0 +1,2 @@
x<01><><EFBFBD>j1<10>S<EFBFBD>)<29>7<EFBFBD><37><EFBFBD><EFBFBD> LH<4C>6U<36>`o<>Kw<>!<21><06>}DH<44>i<06><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFA583><EFBFBD>7P<><50><EFBFBD>b<EFBFBD><62>9%Y<><59>D#Ҹ<><<3C>*Rp<12>AMn<1D><><EFBFBD>5<EFBFBD>n<EFBFBD>4<EFBFBD>Y<EFBFBD>PLy<11>,k<1C><><EFBFBD><EFBFBD>У<D0A3>Wz
|<7C><><EFBFBD>%߇}<1C>Y;m<>R<EFBFBD><52>:<3A>8<><38><EFBFBD><EFBFBD>w}<7D><><EFBFBD>B<EFBFBD>0;?<3F>q泎:<3A><>0<EFBFBD>Ro<52>׾A<D7BE>p<70><7F>4<EFBFBD>V LZ

View File

@@ -0,0 +1 @@
29f2b1020f4b87e1de6aab368ff496cc4e99a959

View File

@@ -0,0 +1 @@
7df4a26d5437d9d4090cd5809967f870444cde8f

View File

@@ -0,0 +1 @@
7df4a26d5437d9d4090cd5809967f870444cde8f