1 /**
2 	Copyright: © 2013 rejectedsoftware e.K.
3 	License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file.
4 	Authors: Sönke Ludwig
5 */
6 module dubregistry.repositories.bitbucket;
7 
8 import dubregistry.cache;
9 import dubregistry.dbcontroller : DbRepository;
10 import dubregistry.repositories.repository;
11 import std.datetime: SysTime;
12 import std.string : format, startsWith;
13 import std.typecons;
14 import vibe.core.log;
15 import vibe.core.stream;
16 import vibe.data.json;
17 import vibe.inet.url;
18 
19 
20 class BitbucketRepository : Repository {
21 @safe:
22 
23 	private {
24 		string m_owner;
25 		string m_project;
26 		string m_authUser;
27 		string m_authPassword;
28 	}
29 
30 	static void register(string user, string password)
31 	{
32 		Repository factory(DbRepository info) @safe {
33 			return new BitbucketRepository(info.owner, info.project, user, password);
34 		}
35 		addRepositoryFactory("bitbucket", &factory);
36 	}
37 
38 	this(string owner, string project, string auth_user, string auth_password)
39 	{
40 		m_owner = owner;
41 		m_project = project;
42 		m_authUser = auth_user;
43 		m_authPassword = auth_password;
44 	}
45 
46 	package Json readPaginatedJson(string url, bool sanitize = false, bool cache_priority = false) @safe {
47 		Json merged = Json.emptyArray;
48 		string nextUrl = url;
49 
50 		while(true) {
51 			Json page = readJson(nextUrl, sanitize, cache_priority);
52 
53 			// foreach(Json value; page["values"] ) {
54 			//     merged ~= value;
55 			// }
56 			merged ~= page["values"];
57 
58 			if("next" in page) {
59 				nextUrl = page["next"].get!string();
60 			} else {
61 				break;
62 			}
63 		}
64 
65 		return merged;
66 	}
67 
68 	package uint readPaginatedLength(string url, bool sanitize = false, bool cache_priority = false) @safe {
69 		Json page = readJson(url, sanitize, cache_priority);
70 		const uint length = page["size"].get!uint();
71 		return length;
72 	}
73 
74 	RefInfo[] extractRefInfo(Json refListJson) {
75 		RefInfo[] ret;
76 		foreach(Json refJson; refListJson.byValue()) {
77 			string refname = refJson["name"].get!string();
78 			try {
79 				Json target = refJson["target"];
80 				string commit_hash = target["hash"].get!string();
81 				auto commit_date = SysTime.fromISOExtString(target["date"].get!string());
82 				ret ~= RefInfo(refname, commit_hash, commit_date);
83 				logDebug("Found ref for %s/%s: %s", m_owner, m_project, refname);
84 			} catch( Exception e ){
85 				throw new Exception("Failed to process ref "~refname~": "~e.msg);
86 			}
87 		}
88 		return ret;
89 	}
90 
91 	RefInfo[] getTags()
92 	{
93 		Json tags;
94 		try tags = readPaginatedJson(getAPIURLPrefix ~ "/2.0/repositories/"~m_owner~"/"~m_project~"/refs/tags");
95 		catch( Exception e ) { throw new Exception("Failed to get tags: "~e.msg); }
96 		RefInfo[] ret = extractRefInfo(tags);
97 		return ret;
98 	}
99 
100 	RefInfo[] getBranches()
101 	{
102 		Json branches = readPaginatedJson(getAPIURLPrefix ~ "/2.0/repositories/"~m_owner~"/"~m_project~"/refs/branches");
103 		RefInfo[] ret = extractRefInfo(branches);
104 		return ret;
105 	}
106 
107 	RepositoryInfo getInfo()
108 	{
109 		Json nfo = readJson(getAPIURLPrefix ~ "/2.0/repositories/"~m_owner~"/"~m_project);
110 		RepositoryInfo ret;
111 		ret.isFork = nfo["is_fork"].opt!bool;
112 		ret.stats.watchers = readPaginatedLength(getAPIURLPrefix ~ "/2.0/repositories/" ~ m_owner ~ "/" ~ m_project ~ "/watchers");
113 		ret.stats.forks = readPaginatedLength(getAPIURLPrefix ~ "/2.0/repositories/" ~ m_owner ~ "/" ~ m_project ~ "/forks");
114 		return ret;
115 	}
116 
117 	RepositoryFile[] listFiles(string commit_sha, InetPath path)
118 	{
119 		assert(path.absolute, "Passed relative path to listFiles.");
120 		auto url = getAPIURLPrefix ~ "/api/2.0/repositories/"~m_owner~"/"~m_project~"/src/"~commit_sha~path.toString()~"?pagelen=100";
121 		auto ls = readJson(url)["values"].get!(Json[]);
122 		RepositoryFile[] ret;
123 		ret.reserve(ls.length);
124 		foreach (entry; ls) {
125 			string type = entry["type"].get!string;
126 			RepositoryFile file;
127 			if (type == "commit_directory") {
128 				file.type = RepositoryFile.Type.directory;
129 			}
130 			else if (type == "commit_file") {
131 				file.type = RepositoryFile.Type.file;
132 				file.size = entry["size"].get!size_t;
133 			}
134 			else continue;
135 			file.commitSha = entry["commit"]["hash"].get!string;
136 			file.path = InetPath("/" ~ entry["path"].get!string);
137 			ret ~= file;
138 		}
139 		return ret;
140 	}
141 
142 	void readFile(string commit_sha, InetPath path, scope void delegate(scope InputStream) @safe reader)
143 	{
144 		assert(path.absolute, "Passed relative path to readFile.");
145 		auto url = getAPIURLPrefix ~ "/2.0/repositories/"~m_owner~"/"~m_project~"/src/"~commit_sha~path.toString();
146 		downloadCached(url, (scope input) @safe {
147 			reader(input);
148 		}, true);
149 	}
150 
151 	string getDownloadUrl(string ver)
152 	{
153 		import std.uri : encodeComponent;
154 		if( ver.startsWith("~") ) ver = ver[1 .. $];
155 		else ver = ver;
156 		auto venc = () @trusted { return encodeComponent(ver); } ();
157 		const url = "https://bitbucket.org/"~m_owner~"/"~m_project~"/get/"~venc~".zip";
158 		if (m_authUser.length) return "https://"~encodeComponent(m_authUser)~":"~encodeComponent(m_authPassword)~"@"~url["https://".length..$];
159 		return url;
160 	}
161 
162 	void download(string ver, scope void delegate(scope InputStream) @safe del)
163 	{
164 		downloadCached(getDownloadUrl(ver), del);
165 	}
166 
167 	private string getAPIURLPrefix()
168 	{
169 		import std.uri : encodeComponent;
170 		if (m_authUser.length) return "https://"~encodeComponent(m_authUser)~":"~encodeComponent(m_authPassword)~"@api.bitbucket.org";
171 		else return "https://api.bitbucket.org";
172 	}
173 }