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 }