Skip to content

Commit 04b57b6

Browse files
committed
Replace Google Drive with GitHub release assets
Google Drive has caused some problems when hosting assets. It has blocked downloads when detecting them as viruses, and hit download limits. GitHub has different policies: At this time, we do not place any limits on the size of binary uploads nor the bandwidth used to deliver them. Via https://help.github.com/articles/distributing-large-binaries
1 parent f1d8a99 commit 04b57b6

10 files changed

+270
-188
lines changed

.gitignore

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Google Drive API credentials
2-
/client_secrets.json
3-
# Generated links go Google Drive files. (upload-to-google-drive.py script)
4-
/google_drive_links
1+
__pycache__/
2+
3+
# Generated by upload-assets
4+
/asset_urls

README.md

+2-6
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,7 @@ slash. Surrounding the value with double quotes is optional.)
7373
* Dependencies such as plugins either built or
7474
[downloaded](https://github.com/freenet/fred/blob/next/src/freenet/pluginmanager/OfficialPlugins.java#L23),
7575
(listing of loadedFrom() CHKs) into FreenetReleased. Repositories using these files can be set up with symlinks.
76-
* That pyDrive is [set up](http://pythonhosted.org/PyDrive/quickstart.html#authentication).
77-
(Already installed by `setup-release-environment`.) Note that this
78-
requires setting a product name and email address on the "APIs & Auth" > "Consent Screen" page.
79-
To avoid the application launching a browser as in the authentication directed by the quick start guide,
80-
create an "installed application" OAuth client ID and change `googleDriveAuth` in `freenetrc` to `"cmdline"`.
76+
* A GitHub OAuth token with `public_repo` access set in `~/.freenetrc` under `gitHubOAuthToken`.
8177
* A jarsigner certificate. This can be a self-signed one, though once (or if) one exists for FPI one should use it. See [here](http://docs.oracle.com/javase/6/docs/technotes/tools/windows/keytool.html). For example: `keytool -genkeypair -keyalg RSA -sigalg SHA256withRSA -keysize 4096 -dname "cn=Robert Releaserson, o=The Freenet Project Inc, c=US" -alias freenet -storepass SomePassphrase -validity 365 -keystore ~/.keystore`
8278
* Set freenetrc `jarsignerAlias` and `jarsignerPassword` to the alias and store passphrase, respectively.
8379
* For the Java installer: [launch4j](http://sourceforge.net/projects/launch4j/)
@@ -137,7 +133,7 @@ If used with `--snapshot` inserts the Fred jar and signature into Freenet.
137133

138134
5. `java -jar [location of released jars]/new_installer_offline_[buildnumber].jar` runs an installer. The release manager should test installing a node both with the Linux / OS X installer and the Windows one. It should be able to bootstrap successfully, access FProxy, and otherwise have no obvious problems.
139135

140-
6. `upload-to-google-drive.py` uploads the jars and installers to Google Drive which serves the majority of downloads.
136+
6. `upload-assets` uploads the jars and installers to GitHub which serves the majority of downloads.
141137

142138
7. `deploy-website`, when run from osprey, updates the website to point to the latest version as defined by the given `fred` repository. The script's `-u` switch updates both `fred` and `website`, so if one wants to avoid pulling in website changes as well it may be preferable to manually update the `fred` repository only, or use the `--force-*-id` options.
143139

deploy-website

+9-9
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,18 @@ popd
148148
#Determine version information.
149149
getBuildInfo
150150

151-
#Read links to release files on Google Drive.
152-
if [ ! -e google_drive_links ]; then
153-
echo "Missing Google Drive links. Please generate them:"
154-
echo "python upload-to-google-drive.py $buildNumber --skip_upload"
151+
# Read URLs of release files.
152+
if [ ! -e asset_urls ]; then
153+
echo "Missing asset URLs. Please generate them:"
154+
echo "./upload-assets $buildNumber"
155155
exit 3
156156
fi
157-
source google_drive_links || exit
157+
source asset_urls || exit
158158

159-
if [[ "$linkBuildNumber" -ne "$buildNumber" ]]; then
160-
echo "The current build is $buildNumber but the links are for $linkBuildNumber"
161-
echo "Please generate them again:"
162-
echo "python upload-to-google-drive.py $buildNumber --skip_upload"
159+
if [[ "$assetBuildNumber" -ne "$buildNumber" ]]; then
160+
echo "The current build is $buildNumber but the URLs are for $assetBuildNumber"
161+
echo "Please generate them:"
162+
echo "./upload-assets $buildNumber"
163163
exit 3
164164
fi
165165

freenetrc-sample

+2-6
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,8 @@ useSshAgent="1"
4444
# Hostname to send updates to.
4545
targetHost="osprey.vm.bytemark.co.uk"
4646

47-
# Google Drive upload authentication method.
48-
# "cmdline": print the URL of a page to be manually opened. This page
49-
# gives a code to type/copy in.
50-
# "browser": open a browser with a URL that allows logging in. This
51-
# avoids having to type in the code.
52-
googleDriveAuth="browser"
47+
# GitHub OAuth token with repo access for uploading assets.
48+
gitHubOAuthToken="token"
5349

5450
# FCP host hostname / IP.
5551
fcpHost="127.0.0.1"

github_releases.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import json
2+
import http.client
3+
import ssl
4+
import os
5+
import mimetypes
6+
7+
8+
class GitHubError(Exception):
9+
"""
10+
GitHub gave a response other than success. Check the response attribute
11+
for details.
12+
"""
13+
def __init__(self, response):
14+
self.response = response
15+
16+
17+
class UnknownMIMETypeError(Exception):
18+
"""
19+
A MIME type for the file could not be detected automatically.
20+
"""
21+
pass
22+
23+
24+
class GitHubReleases(object):
25+
26+
def __init__(self, oauth_token, user_agent):
27+
self.base_headers = {
28+
'Accept': 'application/vnd.github.v3+json',
29+
'Authorization': 'token {}'.format(oauth_token),
30+
'User-Agent': user_agent,
31+
}
32+
33+
context = ssl.create_default_context()
34+
35+
# TODO: Connect once at the start or for each request?
36+
self.api_host = http.client.HTTPSConnection('api.github.com',
37+
context=context)
38+
self.api_host.connect()
39+
40+
self.uploads_host = http.client.HTTPSConnection('uploads.github.com',
41+
context=context)
42+
self.uploads_host.connect()
43+
44+
def create(self, owner, repo, tag):
45+
"""
46+
Create a release from a tag in a repo.
47+
48+
Note that this is a small subset of the GitHub API v3 release creation
49+
capabilities.
50+
51+
:param owner: The owner user or organization.
52+
:param repo: The name of the repository.
53+
:param tag: The tag to create a release from.
54+
"""
55+
self.api_host.request("POST",
56+
"/repos/{}/{}/releases".format(owner, repo),
57+
json.dumps(
58+
{
59+
'tag_name': tag,
60+
'target_commitish': None,
61+
'name': None,
62+
'body': None,
63+
'draft': False,
64+
'prerelease': False,
65+
}), self.base_headers)
66+
return self.__get_response(self.api_host)
67+
68+
def get(self, owner, repo, tag):
69+
"""
70+
Get the release for the given tag.
71+
72+
:param owner: The owner user or organization.
73+
:param repo: The name of the repository.
74+
:param tag: The tag to find the release of.
75+
:return: result for the release with that tag, or None if no such
76+
release.
77+
"""
78+
self.api_host.request("GET",
79+
"/repos/{}/{}/releases".format(owner, repo, tag),
80+
headers=self.base_headers)
81+
response = self.__get_response(self.api_host, expecting_code=200)
82+
83+
for release in response:
84+
if release['tag_name'] == tag:
85+
return release
86+
else:
87+
return None
88+
89+
def upload_asset(self, owner, repo, release_id, asset_path,
90+
asset_type=None):
91+
"""
92+
Upload an asset to a release.
93+
94+
:param owner: The owner user or organization.
95+
:param repo: The name of the repository.
96+
:param release_id: ID of the release to upload to.
97+
:param asset_path: Path to the file to upload.
98+
:param asset_type: Optional MIME type of the asset. If None or not
99+
specified it will be detected.
100+
:throws UnknownMimeTypeError:
101+
"""
102+
filename = os.path.basename(asset_path)
103+
104+
if asset_type is None:
105+
asset_type = mimetypes.guess_type(filename)[0]
106+
if asset_type is None:
107+
raise UnknownMIMETypeError()
108+
109+
headers = {'Content-Type': asset_type}
110+
headers.update(self.base_headers)
111+
112+
self.uploads_host.request(
113+
"POST",
114+
"/repos/{}/{}/releases/{}/assets?name={}"
115+
.format(owner, repo, release_id, filename),
116+
open(asset_path, "rb"),
117+
headers
118+
)
119+
return self.__get_response(self.uploads_host)
120+
121+
def list_assets(self, owner, repo, release_id):
122+
"""
123+
List assets in the release.
124+
125+
:param owner: The owner user or organization.
126+
:param repo: The name of the repository.
127+
:param release_id: ID of the release to list assets from.
128+
:returns: List of assets.
129+
"""
130+
self.api_host.request(
131+
"GET",
132+
"/repos/{}/{}/releases/{}/assets"
133+
.format(owner, repo, release_id),
134+
headers=self.base_headers
135+
)
136+
return self.__get_response(self.api_host, expecting_code=200)
137+
138+
@staticmethod
139+
def __get_response(host, expecting_code=201):
140+
# TODO: Does GitHub _always_ give something that's valid JSON? What
141+
# error handling to have here?
142+
# TODO: What encoding?
143+
response = host.getresponse()
144+
body = response.read()
145+
result = json.loads(body.decode("utf8"))
146+
147+
if response.code != expecting_code:
148+
raise GitHubError(result)
149+
150+
return result

release-build

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ echo Releasing build $buildNumber
3434
release-fred --build || exit 2
3535
release-installer || exit 3
3636
release-wininstaller --release || exit 4
37-
python upload-to-google-drive.py $buildNumber --release_directory "$releaseDir" --auth_type "$googleDriveAuth" || exit 5
37+
env GITHUB_OAUTH="$gitHubOAuthToken" upload-assets $buildNumber "$releaseDir" || exit
3838
# REDFLAG IT IS NEVER SAFE TO INSERT A BUILD ON A DEVELOPMENT NODE!!!!!
3939
echo Now please test the new build.
4040
echo Deploy the build only when:

remote-deploy-website

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ websiteID=$(cd "$websiteDir"; git rev-parse --verify master)
1212
# Both git describe and git rev-parse --verify will give a tag if it exists.
1313
echo Fred ID "$commitID"
1414
echo Website ID "$websiteID"
15-
scp google_drive_links "$targetHost:$remoteDeployPrefix"
15+
scp asset_urls "$targetHost:$remoteDeployPrefix"
1616
ssh $targetHost -- "cd $remoteDeployPrefix; ./deploy-website -u --force-website-id $websiteID --force-fred-id $commitID"

setup-release-environment

+1-7
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,10 @@ source freenet-scripts-common || exit
88
# For release management: rsync, coreutils, diffutils, gnupg, less, wget, perl, python, pip
99
# For building the Windows installer: xvfb, wine, wine-gecko, mono-runtime
1010
# For avoiding repeated password entry: gnupg-agent openssh-client
11-
# For Google Drive upload: PyDrive
1211
# For finding the system certificates: httplib2.ca-certs-locater
1312
# For prompting for things like "2 weeks" in update-version.py: timeparser
1413
# It would be interesting to have the scripts repo without git, but might as well.
15-
sudo apt-get install git openjdk-6-jdk rsync ant coreutils gnupg diffutils less wget xvfb wine perl gnupg-agent openssh-client mono-runtime wine-gecko-1.4 netcat python python-pip || exit
16-
# Unlike 'sudo', 'sudo su' sets umask to the default for root, which
17-
# should be 0022 so that installed files are world-readable.
18-
# TODO: This doesn't actually seem to be the case when checking the umask explicitly: (sudo|) bash -c umask. Is using sudo su appropriate?
19-
# See http://stackoverflow.com/questions/11161776/pip-inconsistent-permissions-issues
20-
sudo su -c 'pip install PyDrive httplib2.ca-certs-locater timeparser' || exit
14+
sudo apt-get install git openjdk-6-jdk rsync ant coreutils gnupg diffutils less wget xvfb wine perl gnupg-agent openssh-client mono-runtime wine-gecko-1.4 netcat python python3 || exit
2115

2216
mkdir -p "$releaseDir/dependencies"
2317
wget "https://people.apache.org/~ebourg/jsign/jsign-1.2.jar" -O "$releaseDir/dependencies/jsign-1.2.jar"

upload-assets

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import os
4+
5+
import github_releases
6+
7+
parser = argparse.ArgumentParser(description="""
8+
Upload a release to GitHub: the .jar, the source archive (so the signatures
9+
will match it), the Java installer, and the Windows installer. If no directory
10+
is specified it will read existing assets.
11+
12+
It assumes the build tag is 'build' followed by the build number
13+
zero-padded to 5 characters.
14+
""")
15+
parser.add_argument('build_number', help='The build number to upload.',
16+
type=int)
17+
parser.add_argument('release_directory',
18+
help='Path to the FreenetReleased directory containing '
19+
'the assets to upload.', nargs='?', default=None)
20+
parser.add_argument('--out', default='asset_urls',
21+
help='File to output URLs to')
22+
23+
args = parser.parse_args()
24+
build_tag = 'build{0:05d}'.format(args.build_number)
25+
26+
# TODO: Allow resuming: avoid clobbering existing assets when uploading.
27+
# TODO: Nicer error on missing environment variable.
28+
# TODO: Bind owner and repo to GitHubReleases instance? They're in every call.
29+
# TODO: Restructure the body of this into a module?
30+
# TODO: The name 'upload-assets' makes the non-upload behavior surprising.
31+
# What's a better name for this script?
32+
conn = github_releases.GitHubReleases(os.environ['GITHUB_OAUTH'],
33+
'Freenet-Release-Uploader')
34+
35+
# Create a release at the tag, or get its ID if one already exists.
36+
try:
37+
release = conn.create('freenet', 'fred', build_tag)
38+
release_id = release['id']
39+
print('Created release {}'.format(release_id))
40+
except github_releases.GitHubError as e:
41+
# TODO: Avoid hardcoding here?
42+
if e.response['errors'][0]['code'] == 'already_exists':
43+
release_id = conn.get('freenet', 'fred', build_tag)['id']
44+
print('Found release {}'.format(release_id))
45+
else:
46+
raise
47+
48+
files = {
49+
'jar': 'freenet-{0}.jar'.format(build_tag),
50+
'source': 'freenet-{0}-source.tar.bz2'.format(build_tag),
51+
'java_installer': 'new_installer_offline_{0}.jar'.format(args.build_number),
52+
'windows_installer': 'FreenetInstaller-{0}.exe'.format(args.build_number),
53+
}
54+
urls = {}
55+
56+
# Upload if the release directory is specified, otherwise find existing assets.
57+
if args.release_directory is not None:
58+
for name, file in files.items():
59+
path = os.path.join(args.release_directory, file)
60+
61+
print("Uploading asset {}".format(file))
62+
result = conn.upload_asset("freenet", "fred", release_id, path)
63+
64+
urls[name] = result['url']
65+
else:
66+
result = conn.list_assets("freenet", "fred", release_id)
67+
68+
assets = {}
69+
for asset in result:
70+
assets[asset['name']] = asset['browser_download_url']
71+
72+
for name, file in files.items():
73+
if file not in assets:
74+
print('Missing asset: {}'.format(file))
75+
exit(1)
76+
77+
print("Found asset {}".format(file))
78+
urls[name + "_url"] = assets[file]
79+
80+
# Each file must have a URL.
81+
assert len(files) == len(urls)
82+
83+
# Output URLs for use in substitution.
84+
# TODO: Verbosity vs readability with something like this?
85+
with open(args.out, 'w') as output:
86+
total = files.copy()
87+
total.update(urls)
88+
89+
output.write("""\
90+
# Generated for build {build_number} with upload-assets
91+
assetBuildNumber={build_number}
92+
FREENET_MAIN_JAR_URL={jar_url}
93+
FREENET_SOURCE_URL={source_url}
94+
FREENET_INSTALLER_URL={java_installer_url}
95+
FREENET_WINDOWS_INSTALLER_URL={windows_installer_url}
96+
FREENET_MAIN_JAR_SIG_URL=https://downloads.freenetproject.org/alpha/{jar}.sig
97+
FREENET_SOURCE_SIG_URL=https://downloads.freenetproject.org/alpha/{source}.sig
98+
FREENET_INSTALLER_SIG_URL=https://downloads.freenetproject.org/alpha/installer/{java_installer}.sig
99+
FREENET_WINDOWS_INSTALLER_SIG_URL=https://downloads.freenetproject.org/alpha/installer/{windows_installer}.sig
100+
""".format(build_number=args.build_number, **total))

0 commit comments

Comments
 (0)