Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
05c0f98a56
23 changed files with 293 additions and 265 deletions
|
@ -4,7 +4,7 @@ to prepare a new host:
|
||||||
sudo apt-get install software-properties-common
|
sudo apt-get install software-properties-common
|
||||||
sudo apt-add-repository -y ppa:nginx/stable
|
sudo apt-add-repository -y ppa:nginx/stable
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git python-virtualenv python-dev phantomjs
|
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev
|
||||||
sudo apt-get install -y nginx-full
|
sudo apt-get install -y nginx-full
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -21,12 +21,9 @@ running:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo nginx -c `pwd`/nginx.conf
|
sudo nginx -c `pwd`/nginx.conf
|
||||||
STACK=prod gunicorn -D --workers 4 -b unix:/tmp/gunicorn.sock --worker-class eventlet -t 2000 application:application
|
STACK=prod gunicorn -D --workers 4 -b unix:/tmp/gunicorn.sock --worker-class gevent -t 2000 application:application
|
||||||
```
|
```
|
||||||
|
|
||||||
set up the snapshot script:
|
|
||||||
(instructions in the seo-snapshots directory)[seo-snapshots/README.md]
|
|
||||||
|
|
||||||
start the workers:
|
start the workers:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -4,13 +4,17 @@ import stripe
|
||||||
|
|
||||||
from flask import (abort, redirect, request, url_for, render_template,
|
from flask import (abort, redirect, request, url_for, render_template,
|
||||||
make_response, Response)
|
make_response, Response)
|
||||||
from flask.ext.login import login_user, UserMixin, login_required
|
from flask.ext.login import login_user, UserMixin
|
||||||
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
|
from flask.ext.principal import identity_changed
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, login_manager, mixpanel
|
from app import app, login_manager, mixpanel
|
||||||
from auth.permissions import QuayDeferredPermissionUser, AdministerOrganizationPermission
|
from auth.permissions import (QuayDeferredPermissionUser,
|
||||||
|
AdministerOrganizationPermission)
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
|
from util.seo import render_snapshot
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -49,6 +53,19 @@ def index(path):
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/snapshot', methods=['GET'])
|
||||||
|
@app.route('/snapshot/', methods=['GET'])
|
||||||
|
@app.route('/snapshot/<path:path>', methods=['GET'])
|
||||||
|
def snapshot(path = ''):
|
||||||
|
parsed = urlparse(request.url)
|
||||||
|
final_url = '%s://%s/%s' % (parsed.scheme, 'localhost', path)
|
||||||
|
result = render_snapshot(final_url)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/plans/')
|
@app.route('/plans/')
|
||||||
def plans():
|
def plans():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
|
@ -47,7 +47,7 @@ http {
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
if ($args ~ "_escaped_fragment_") {
|
if ($args ~ "_escaped_fragment_") {
|
||||||
rewrite ^ /static/snapshots$uri/index.html;
|
rewrite ^ /snapshot$uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
|
|
|
@ -9,7 +9,7 @@ boto
|
||||||
pymysql
|
pymysql
|
||||||
stripe
|
stripe
|
||||||
gunicorn
|
gunicorn
|
||||||
eventlet
|
gevent
|
||||||
mixpanel-py
|
mixpanel-py
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
marisa-trie
|
marisa-trie
|
||||||
|
|
|
@ -14,7 +14,7 @@ blinker==1.3
|
||||||
boto==2.17.0
|
boto==2.17.0
|
||||||
distribute==0.6.34
|
distribute==0.6.34
|
||||||
ecdsa==0.10
|
ecdsa==0.10
|
||||||
eventlet==0.14.0
|
gevent==0.13.8
|
||||||
greenlet==0.4.1
|
greenlet==0.4.1
|
||||||
gunicorn==18.0
|
gunicorn==18.0
|
||||||
html5lib==1.0b3
|
html5lib==1.0b3
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
Follow the instructions to set up a host of the whole project before attempting to run.
|
|
||||||
|
|
||||||
to run once:
|
|
||||||
|
|
||||||
```
|
|
||||||
python make_snapshot.py
|
|
||||||
```
|
|
||||||
|
|
||||||
cron line to update every 30 minutes:
|
|
||||||
|
|
||||||
```
|
|
||||||
0,30 * * * * cd /home/ubuntu/quay/seo-snapshots && ../venv/bin/python make_snapshot.py
|
|
||||||
```
|
|
|
@ -1,60 +0,0 @@
|
||||||
import subprocess
|
|
||||||
import urllib
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
BASE_URL = 'http://localhost:5000'
|
|
||||||
OUTPUT_PATH = 'snapshots/'
|
|
||||||
|
|
||||||
aware_of = set()
|
|
||||||
crawl_queue = Queue()
|
|
||||||
|
|
||||||
def crawl_url(url):
|
|
||||||
final_url = BASE_URL + url
|
|
||||||
to_write = OUTPUT_PATH + url + 'index.html'
|
|
||||||
|
|
||||||
logger.info('Snapshotting url: %s -> %s' % (final_url, to_write))
|
|
||||||
|
|
||||||
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
|
|
||||||
'phantomjs-runner.js', final_url])
|
|
||||||
|
|
||||||
# Remove script tags
|
|
||||||
soup = BeautifulSoup(out_html)
|
|
||||||
to_extract = soup.findAll('script')
|
|
||||||
for item in to_extract:
|
|
||||||
item.extract()
|
|
||||||
|
|
||||||
# Find all links and add them to the crawl queue
|
|
||||||
for link in soup.findAll('a'):
|
|
||||||
to_add = link.get('href')
|
|
||||||
|
|
||||||
if to_add not in aware_of and to_add.startswith('/'):
|
|
||||||
logger.info('Adding link to be crawled: %s' % to_add)
|
|
||||||
crawl_queue.put(to_add)
|
|
||||||
aware_of.add(to_add)
|
|
||||||
|
|
||||||
to_write_dir = os.path.dirname(to_write)
|
|
||||||
|
|
||||||
if not os.path.exists(to_write_dir):
|
|
||||||
os.makedirs(to_write_dir)
|
|
||||||
|
|
||||||
with codecs.open(to_write, 'w', 'utf-8') as output_file:
|
|
||||||
output_file.write(soup.prettify())
|
|
||||||
|
|
||||||
# Seed the crawler
|
|
||||||
crawl_queue.put('/')
|
|
||||||
aware_of.add('/')
|
|
||||||
|
|
||||||
# Crawl
|
|
||||||
while not crawl_queue.empty():
|
|
||||||
to_crawl = crawl_queue.get()
|
|
||||||
crawl_url(to_crawl)
|
|
|
@ -1,46 +0,0 @@
|
||||||
import subprocess
|
|
||||||
import urllib
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
BASE_URL = 'https://localhost/'
|
|
||||||
OUTPUT_PATH = '../static/snapshots/'
|
|
||||||
|
|
||||||
URLS = [
|
|
||||||
'',
|
|
||||||
'guide/',
|
|
||||||
'plans/',
|
|
||||||
'repository/',
|
|
||||||
'signin/',
|
|
||||||
]
|
|
||||||
|
|
||||||
for url in URLS:
|
|
||||||
final_url = BASE_URL + url
|
|
||||||
to_write = OUTPUT_PATH + url + 'index.html'
|
|
||||||
|
|
||||||
logger.info('Snapshotting url: %s -> %s' % (final_url, to_write))
|
|
||||||
|
|
||||||
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
|
|
||||||
'phantomjs-runner.js', final_url])
|
|
||||||
|
|
||||||
# Remove script tags
|
|
||||||
soup = BeautifulSoup(out_html)
|
|
||||||
to_extract = soup.findAll('script')
|
|
||||||
for item in to_extract:
|
|
||||||
item.extract()
|
|
||||||
|
|
||||||
to_write_dir = os.path.dirname(to_write)
|
|
||||||
|
|
||||||
if not os.path.exists(to_write_dir):
|
|
||||||
os.makedirs(to_write_dir)
|
|
||||||
|
|
||||||
with codecs.open(to_write, 'w', 'utf-8') as output_file:
|
|
||||||
output_file.write(soup.prettify())
|
|
|
@ -1,23 +0,0 @@
|
||||||
var system = require('system');
|
|
||||||
var url = system.args[1] || '';
|
|
||||||
if(url.length > 0) {
|
|
||||||
var page = require('webpage').create();
|
|
||||||
page.open(url, function (status) {
|
|
||||||
if (status == 'success') {
|
|
||||||
var delay, checker = (function() {
|
|
||||||
var html = page.evaluate(function () {
|
|
||||||
var ready = document.getElementsByClassName('ready-indicator')[0];
|
|
||||||
if(ready.getAttribute('data-status') == 'ready') {
|
|
||||||
return document.getElementsByTagName('html')[0].outerHTML;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(html) {
|
|
||||||
clearTimeout(delay);
|
|
||||||
console.log(html);
|
|
||||||
phantom.exit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delay = setInterval(checker, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -673,8 +673,8 @@ form input.ng-valid.ng-dirty,
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repoSearch {
|
.repo-search-box {
|
||||||
width: 300px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-mini-listing {
|
.repo-mini-listing {
|
||||||
|
|
|
@ -22,15 +22,17 @@
|
||||||
|
|
||||||
|
|
||||||
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
|
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
|
||||||
|
<li>
|
||||||
<form class="navbar-form navbar-left" role="search">
|
<form class="navbar-form navbar-left" role="search">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo">
|
<span class="repo-search"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
|
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
|
||||||
</span>
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dropdown" ng-switch-when="false">
|
<li class="dropdown" ng-switch-when="false">
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
|
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
|
|
@ -1 +1 @@
|
||||||
<span class="markdown-view-content" ng-bind-html-unsafe="getMarkedDown(content, firstLineOnly)"></span>
|
<span class="markdown-view-content" ng-bind-html="getMarkedDown(content, firstLineOnly)"></span>
|
||||||
|
|
1
static/directives/repo-search.html
Normal file
1
static/directives/repo-search.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<input type="text" class="form-control repo-search-box" placeholder="Find Repo">
|
146
static/js/app.js
146
static/js/app.js
|
@ -47,7 +47,7 @@ function getMarkedDown(string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the application code itself.
|
// Start the application code itself.
|
||||||
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
|
quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
|
||||||
$provide.factory('UserService', ['Restangular', function(Restangular) {
|
$provide.factory('UserService', ['Restangular', function(Restangular) {
|
||||||
var userResponse = {
|
var userResponse = {
|
||||||
verified: false,
|
verified: false,
|
||||||
|
@ -399,22 +399,23 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||||
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
|
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
|
||||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
|
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
|
||||||
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
|
||||||
when('/user/', {title: 'Account Settings', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||||
when('/guide/', {title: 'Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
when('/user/', {title: 'Account Settings', description:'Account settings for Quay', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
||||||
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
||||||
when('/signin/', {title: 'Sign In', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay',
|
||||||
when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||||
|
when('/signin/', {title: 'Sign In', description: 'Sign into Quay', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
||||||
when('/organizations/', {title: 'Organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
|
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
|
||||||
when('/organizations/new/', {title: 'New Organization', templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
|
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
||||||
|
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
||||||
|
templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
|
||||||
|
when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on Quay',
|
||||||
|
templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
|
||||||
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
|
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
|
||||||
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
|
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
|
||||||
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
|
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
|
||||||
|
|
||||||
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
|
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
|
||||||
|
|
||||||
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
}]).
|
}]).
|
||||||
|
@ -434,12 +435,12 @@ quayApp.directive('markdownView', function () {
|
||||||
'content': '=content',
|
'content': '=content',
|
||||||
'firstLineOnly': '=firstLineOnly'
|
'firstLineOnly': '=firstLineOnly'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element, $sce) {
|
||||||
$scope.getMarkedDown = function(content, firstLineOnly) {
|
$scope.getMarkedDown = function(content, firstLineOnly) {
|
||||||
if (firstLineOnly) {
|
if (firstLineOnly) {
|
||||||
content = getFirstTextLine(content);
|
content = getFirstTextLine(content);
|
||||||
}
|
}
|
||||||
return getMarkedDown(content);
|
return $sce.trustAsHtml(getMarkedDown(content));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -595,6 +596,103 @@ quayApp.directive('markdownInput', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('repoSearch', function () {
|
||||||
|
var number = 0;
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/repo-search.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, $location, UserService, Restangular) {
|
||||||
|
var searchToken = 0;
|
||||||
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
++searchToken;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
var element = $($element[0].childNodes[0]);
|
||||||
|
element.typeahead({
|
||||||
|
name: 'repositories',
|
||||||
|
remote: {
|
||||||
|
url: '/api/find/repository?query=%QUERY',
|
||||||
|
replace: function (url, uriEncodedQuery) {
|
||||||
|
url = url.replace('%QUERY', uriEncodedQuery);
|
||||||
|
url += '&cb=' + searchToken;
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
filter: function(data) {
|
||||||
|
var datums = [];
|
||||||
|
for (var i = 0; i < data.repositories.length; ++i) {
|
||||||
|
var repo = data.repositories[i];
|
||||||
|
datums.push({
|
||||||
|
'value': repo.name,
|
||||||
|
'tokens': [repo.name, repo.namespace],
|
||||||
|
'repo': repo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return datums;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: function (datum) {
|
||||||
|
template = '<div class="repo-mini-listing">';
|
||||||
|
template += '<i class="fa fa-hdd fa-lg"></i>'
|
||||||
|
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
||||||
|
if (datum.repo.description) {
|
||||||
|
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
|
||||||
|
}
|
||||||
|
|
||||||
|
template += '</div>'
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
element.on('typeahead:selected', function (e, datum) {
|
||||||
|
element.typeahead('setQuery', '');
|
||||||
|
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('headerBar', function () {
|
||||||
|
var number = 0;
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/header-bar.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, $location, UserService, Restangular) {
|
||||||
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
$scope.user = currentUser;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.signout = function() {
|
||||||
|
var signoutPost = Restangular.one('signout');
|
||||||
|
signoutPost.customPOST().then(function() {
|
||||||
|
UserService.load();
|
||||||
|
$location.path('/');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.appLinkTarget = function() {
|
||||||
|
if ($("div[ng-view]").length === 0) {
|
||||||
|
return "_self";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('entitySearch', function () {
|
quayApp.directive('entitySearch', function () {
|
||||||
var number = 0;
|
var number = 0;
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
|
@ -1053,7 +1151,8 @@ quayApp.directive('ngBlur', function() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', function($location, $rootScope, Restangular, UserService) {
|
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', '$http',
|
||||||
|
function($location, $rootScope, Restangular, UserService, $http) {
|
||||||
Restangular.setErrorInterceptor(function(response) {
|
Restangular.setErrorInterceptor(function(response) {
|
||||||
if (response.status == 401) {
|
if (response.status == 401) {
|
||||||
$('#sessionexpiredModal').modal({});
|
$('#sessionexpiredModal').modal({});
|
||||||
|
@ -1067,5 +1166,20 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', function($
|
||||||
if (current.$$route.title) {
|
if (current.$$route.title) {
|
||||||
$rootScope.title = current.$$route.title;
|
$rootScope.title = current.$$route.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current.$$route.description) {
|
||||||
|
$rootScope.description = current.$$route.description;
|
||||||
|
} else {
|
||||||
|
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var initallyChecked = false;
|
||||||
|
window.__isLoading = function() {
|
||||||
|
if (!initallyChecked) {
|
||||||
|
initallyChecked = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $http.pendingRequests.length > 0;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
$.fn.clipboardCopy = function() {
|
$.fn.clipboardCopy = function() {
|
||||||
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
||||||
|
|
||||||
clip.on('complete', function() {
|
clip.on('complete', function() {
|
||||||
// Resets the animation.
|
// Resets the animation.
|
||||||
var elem = $('#clipboardCopied')[0];
|
var elem = $('#clipboardCopied')[0];
|
||||||
|
@ -14,74 +15,6 @@ $.fn.clipboardCopy = function() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function HeaderCtrl($scope, $location, UserService, Restangular) {
|
|
||||||
var searchToken = 0;
|
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
|
||||||
$scope.user = currentUser;
|
|
||||||
++searchToken;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
$scope.signout = function() {
|
|
||||||
var signoutPost = Restangular.one('signout');
|
|
||||||
signoutPost.customPOST().then(function() {
|
|
||||||
UserService.load();
|
|
||||||
$location.path('/');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.appLinkTarget = function() {
|
|
||||||
if ($("div[ng-view]").length === 0) {
|
|
||||||
return "_self";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.$on('$includeContentLoaded', function() {
|
|
||||||
// THIS IS BAD, MOVE THIS TO A DIRECTIVE
|
|
||||||
$('#repoSearch').typeahead({
|
|
||||||
name: 'repositories',
|
|
||||||
remote: {
|
|
||||||
url: '/api/find/repository?query=%QUERY',
|
|
||||||
replace: function (url, uriEncodedQuery) {
|
|
||||||
url = url.replace('%QUERY', uriEncodedQuery);
|
|
||||||
url += '&cb=' + searchToken;
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
filter: function(data) {
|
|
||||||
var datums = [];
|
|
||||||
for (var i = 0; i < data.repositories.length; ++i) {
|
|
||||||
var repo = data.repositories[i];
|
|
||||||
datums.push({
|
|
||||||
'value': repo.name,
|
|
||||||
'tokens': [repo.name, repo.namespace],
|
|
||||||
'repo': repo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return datums;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: function (datum) {
|
|
||||||
template = '<div class="repo-mini-listing">';
|
|
||||||
template += '<i class="fa fa-hdd fa-lg"></i>'
|
|
||||||
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
|
||||||
if (datum.repo.description) {
|
|
||||||
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
template += '</div>'
|
|
||||||
return template;
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#repoSearch').on('typeahead:selected', function (e, datum) {
|
|
||||||
$('#repoSearch').typeahead('setQuery', '');
|
|
||||||
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) {
|
function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) {
|
||||||
$scope.sendRecovery = function() {
|
$scope.sendRecovery = function() {
|
||||||
var signinPost = Restangular.one('recovery');
|
var signinPost = Restangular.one('recovery');
|
||||||
|
@ -307,6 +240,11 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
||||||
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
||||||
repositoryFetch.get().then(function(repo) {
|
repositoryFetch.get().then(function(repo) {
|
||||||
$rootScope.title = namespace + '/' + name;
|
$rootScope.title = namespace + '/' + name;
|
||||||
|
|
||||||
|
var kind = repo.is_public ? 'public' : 'private';
|
||||||
|
$rootScope.description = jQuery(getFirstTextLine(repo.description)).text() ||
|
||||||
|
'View of a ' + kind + ' docker repository on Quay';
|
||||||
|
|
||||||
$scope.repo = repo;
|
$scope.repo = repo;
|
||||||
|
|
||||||
$scope.setTag($routeParams.tag);
|
$scope.setTag($routeParams.tag);
|
||||||
|
@ -666,6 +604,8 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/');
|
var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/');
|
||||||
permissionsFetch.get().then(function(resp) {
|
permissionsFetch.get().then(function(resp) {
|
||||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||||
|
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
||||||
|
': Permissions, web hooks and other settings';
|
||||||
$scope.permissions[kind] = resp.permissions;
|
$scope.permissions[kind] = resp.permissions;
|
||||||
checkLoading();
|
checkLoading();
|
||||||
}, function() {
|
}, function() {
|
||||||
|
@ -890,6 +830,8 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
|
||||||
};
|
};
|
||||||
$scope.image = image;
|
$scope.image = image;
|
||||||
$rootScope.title = 'View Image - ' + image.id;
|
$rootScope.title = 'View Image - ' + image.id;
|
||||||
|
$rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
|
||||||
|
': Image changes tree and list view';
|
||||||
}, function() {
|
}, function() {
|
||||||
$rootScope.title = 'Unknown Image';
|
$rootScope.title = 'Unknown Image';
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
|
@ -1068,13 +1010,14 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
||||||
|
|
||||||
$scope.upgradePlan = function() {
|
$scope.upgradePlan = function() {
|
||||||
var callbacks = {
|
var callbacks = {
|
||||||
|
'started': function() { $scope.planChanging = true; },
|
||||||
'opened': function() { $scope.planChanging = true; },
|
'opened': function() { $scope.planChanging = true; },
|
||||||
'closed': function() { $scope.planChanging = false; },
|
'closed': function() { $scope.planChanging = false; },
|
||||||
'success': subscribedToPlan,
|
'success': subscribedToPlan,
|
||||||
'failure': function() { $('#couldnotsubscribeModal').modal(); $scope.planChanging = false; }
|
'failure': function() { $('#couldnotsubscribeModal').modal(); $scope.planChanging = false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, null, callbacks);
|
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
||||||
|
@ -1128,6 +1071,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
|
|
||||||
$rootScope.title = orgname;
|
$rootScope.title = orgname;
|
||||||
|
$rootScope.description = 'Viewing organization ' + orgname;
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
});
|
});
|
||||||
|
@ -1279,6 +1223,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
||||||
if (resp && resp.is_admin) {
|
if (resp && resp.is_admin) {
|
||||||
$scope.organization = resp;
|
$scope.organization = resp;
|
||||||
$rootScope.title = orgname + ' (Admin)';
|
$rootScope.title = orgname + ' (Admin)';
|
||||||
|
$rootScope.description = 'Administration page for organization ' + orgname;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
|
@ -1356,6 +1301,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
||||||
$scope.canEditMembers = resp.can_edit;
|
$scope.canEditMembers = resp.can_edit;
|
||||||
$scope.loading = !$scope.organization || !$scope.members;
|
$scope.loading = !$scope.organization || !$scope.members;
|
||||||
$rootScope.title = teamname + ' (' + orgname + ')';
|
$rootScope.title = teamname + ' (' + orgname + ')';
|
||||||
|
$rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + orgname;
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.organization = null;
|
$scope.organization = null;
|
||||||
$scope.members = null;
|
$scope.members = null;
|
||||||
|
|
15
static/lib/ZeroClipboard.min.js
vendored
15
static/lib/ZeroClipboard.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -10,7 +10,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
@ -38,7 +37,8 @@
|
||||||
<script src="//code.jquery.com/jquery.js"></script>
|
<script src="//code.jquery.com/jquery.js"></script>
|
||||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
|
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
|
||||||
|
|
||||||
|
@ -74,8 +74,7 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Nav bar -->
|
<!-- Nav bar -->
|
||||||
<nav class="navbar navbar-default" role="navigation" ng-include="'/static/partials/header.html'" ng-controller='HeaderCtrl' >
|
<nav class="navbar navbar-default header-bar" role="navigation"></nav>
|
||||||
</nav>
|
|
||||||
|
|
||||||
{% block body_content %}
|
{% block body_content %}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
{% block added_meta %}
|
{% block added_meta %}
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
|
<meta name="description" content="{% raw %}{{ description }}{% endraw %}"></meta>
|
||||||
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
||||||
<meta name="fragment" content="!" />
|
<meta name="fragment" content="!" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -20,8 +21,7 @@
|
||||||
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/zeroclipboard/1.1.7/ZeroClipboard.min.js"></script>
|
<script src="static/lib/ZeroClipboard.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/jquery.overscroll.min.js"></script>
|
<script src="static/lib/jquery.overscroll.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
|
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
<title>Privacy Policy · Quay.io</title>
|
<title>Privacy Policy · Quay.io</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block added_meta %}
|
||||||
|
<meta name="description" content="Privacy policy for Quay - Hosted private docker repository">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_content %}
|
{% block body_content %}
|
||||||
<div class="container privacy-policy">
|
<div class="container privacy-policy">
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
<title>Terms of Service · Quay.io</title>
|
<title>Terms of Service · Quay.io</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block added_meta %}
|
||||||
|
<meta name="description" content="Terms of service for Quay - Hosted private docker repository">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_content %}
|
{% block body_content %}
|
||||||
<div class="tos container">
|
<div class="tos container">
|
||||||
<h2>Terms of Service</h2>
|
<h2>Terms of Service</h2>
|
||||||
|
|
25
tools/freeloaders.py
Normal file
25
tools/freeloaders.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from data import model
|
||||||
|
from data.database import User
|
||||||
|
from app import stripe
|
||||||
|
from data.plans import get_plan
|
||||||
|
|
||||||
|
def get_private_allowed(customer):
|
||||||
|
if not customer.stripe_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
subscription = stripe.Customer.retrieve(customer.stripe_id).subscription
|
||||||
|
if subscription is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
plan = get_plan(subscription.plan.id)
|
||||||
|
return plan['privateRepos']
|
||||||
|
|
||||||
|
# Find customers who have more private repositories than their plans allow
|
||||||
|
users = User.select()
|
||||||
|
|
||||||
|
usage = [(user.username, model.get_private_repo_count(user.username),
|
||||||
|
get_private_allowed(user)) for user in users]
|
||||||
|
|
||||||
|
for username, used, allowed in usage:
|
||||||
|
if used > allowed:
|
||||||
|
print('Violation: %s %s > %s' % (username, used, allowed))
|
37
util/phantomjs-runner.js
Normal file
37
util/phantomjs-runner.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
var system = require('system');
|
||||||
|
var url = system.args[1] || '';
|
||||||
|
if(url.length > 0) {
|
||||||
|
var page = require('webpage').create();
|
||||||
|
page.open(url, function (status) {
|
||||||
|
if (status == 'success') {
|
||||||
|
var delay, checker = (function() {
|
||||||
|
var html = page.evaluate(function () {
|
||||||
|
var found = document.getElementsByTagName('html')[0].outerHTML || '';
|
||||||
|
if (window.__isLoading && !window.__isLoading()) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
if (found.indexOf('404 Not Found') > 0) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
if (html.indexOf('404 Not Found') > 0) {
|
||||||
|
console.log('Not Found');
|
||||||
|
phantom.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(delay);
|
||||||
|
console.log(html);
|
||||||
|
phantom.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
delay = setInterval(checker, 100);
|
||||||
|
} else {
|
||||||
|
console.log('Not Found');
|
||||||
|
phantom.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
23
util/seo.py
Normal file
23
util/seo.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
def render_snapshot(url):
|
||||||
|
logger.info('Snapshotting url: %s' % url)
|
||||||
|
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
|
||||||
|
'util/phantomjs-runner.js', url])
|
||||||
|
|
||||||
|
if not out_html or out_html.strip() == 'Not Found':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Remove script tags
|
||||||
|
soup = BeautifulSoup(out_html.decode('utf8'))
|
||||||
|
to_extract = soup.findAll('script')
|
||||||
|
for item in to_extract:
|
||||||
|
item.extract()
|
||||||
|
|
||||||
|
return str(soup)
|
Reference in a new issue