使用GitLabApi获取远程仓库中的文件内容

在一些实际情况中,希望能够直接像读取本地文件一样读取远程仓库中的文件内容,避免git操作失败的情况下读取的本地缓存的文件内容。由于项目使用gitLab管理配置文件,查询了GitLabApi,其提供了诸多API接口,包括常见的git操作、项目管理以及我们需要的获取文件内容等接口。

1.接口分析

查询GitLab api,可以容易找到获取文件内容的API文档:GitLab获取仓库中文件内容,可以发现,其格式要求为:

GET /projects/:id/repository/files/:file_path
curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'

通过分析可以发现,如果想获取仓库中文件内容,需要以下几个要素:

  • 仓库地址
  • 项目id
  • 用户的private token
  • 经过url编码的文件全路径
  • 文件所在的分支
    由此,我在实现时将API接口整理为一个常量:
private static String GITLAB_FILECONTENT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_ID}/repository/files?private_token=#{PRIVATE_TOKEN}&file_path=#{FILE_PATH}&ref=#{BRANCH_NAME}";

2.获取用户的private token

GitLabApi关于获取个人口令的API可以整理为常量类:

private static String GITLAB_SESSION_API = "http://#{REPO_IP}/api/v3/session?login=#{USER_NAME}&password=#{PASSWORD}";

提供如下3个变量即可获取指定用户的token:

  • 仓库地址
  • 用户名
  • 密码
    经过简单的json解析就能获取到结果,代码如下:
/**
     * 根据用户名称和密码获取gitlab的private token,为Post请求
     * 
     * @param ip    gitlab仓库的ip
     * @param userName  登陆gitlab的用户名
     * @param password  登陆gitlab的密码
     * @return  返回该用户的private token
     */
    public static String getPrivateTokenByPassword(String ip, String userName, String password) {

        /** 1.参数替换,生成获取指定用户privateToken地址 */
        //  校验参数
        Objects.requireNonNull(ip, "参数ip不能为空!");
        Objects.requireNonNull(userName, "参数userName不能为空!");
        Objects.requireNonNull(password, "参数password不能为空!");
        //  参数准备,存入map
        Map<String, String> params = new HashMap<String, String>(4);
        params.put("REPO_IP", ip);
        params.put("USER_NAME", userName);
        params.put("PASSWORD", password);
        // 调用工具类替换,得到具体的调用地址
        String reqUserTokenUrl = PlaceholderUtil.anotherReplace(GITLAB_SESSION_API, params);
        sysLogger.debug(String.format("获取用户:%s的private token地址为:%s", userName,reqUserTokenUrl));
        
        /** 2.访问url,获取指定用户的信息 */
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.postForEntity(reqUserTokenUrl, null,String.class);
        sysLogger.debug(String.format("响应头为:%s,响应体为:%s", response.getHeaders(), response.getBody()));
        

        /** 3.解析结果 */
        String body = response.getBody();
        JSONObject jsonBody = JsonUtil.parseObjectToJSONObject(body);
        String privateToken =jsonBody.getString("private_token");
        sysLogger.debug(String.format("获取到用户:%s的privateToken为:%s", userName, privateToken));
        
        /** 4.返回privateToken */     
        return privateToken;

    }

注意,这个接口时POST请求,正确的HTTP响应码是201,而非200.

3.获取项目的projectId

projectId可以直接到gitlab具体项目信息中查找,但是也有相关的api可以依据项目名称查询。从使用的角度讲,配置项目的名称更加方便,配置项目的projectId增加配置项,且如果提供获取projectId的方法,内部调用可以去掉该项的配置。
获取projectId的api参考:获取指定项目的projectId,需要如下3个要素:

  • 仓库ip
  • 项目id:The ID or URL-encoded path of the project,即提供id或者是项目path,需要经url编码(namespace + projectName),参见:项目path的url编码
  • private token
    提取出常量类:
private static String GITLAB_SINGLE_PROJECT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_PATH}?private_token=#{PRIVATE_TOKEN}";

然后对响应码为200的正确json返回结果信息解析即可得到,代码如下:

/**
     * 使用gitLab api获取指定项目的projectId,为Get请求
     * 
     * @param ip  项目仓库的ip地址
     * @param projectPath   项目的path,如:http://192.168.59.185/acountting/dispatcher-cloud.git,则projectPath为:acountting/dispatcher-cloud
     * @param privateToken  用户个人访问gitlab库时的privateToken,可以通过{@link GitLabAPIUtils#getPrivateTokenByPassword}获取
     * @return  返回目的projectId
     */
    public static String getProjectId(String ip, String projectPath, String privateToken) {
        /** 1.参数替换,生成访问获取project信息的uri地址 */
        //  校验参数
        Objects.requireNonNull(ip, "参数ip不能为空!");
        Objects.requireNonNull(projectPath, "参数projectPath不能为空!");
        Objects.requireNonNull(privateToken, "参数privateToken不能为空!");
        //  参数准备,存入map
        Map<String, String> params = new HashMap<String, String>(4);
        params.put("REPO_IP", ip);
        params.put("PRIVATE_TOKEN", privateToken);      
        // gitlab api要求项目的path需要安装uri编码格式进行编码,比如"/"编码为"%2F"
        try {
            params.put("PROJECT_PATH", URLEncoder.encode(projectPath, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(String.format("对%s进行URI编码出错!", projectPath));
        }
        // 调用工具类替换,得到具体的调用地址
        String getSingleProjectUrl = PlaceholderUtil.anotherReplace(GITLAB_SINGLE_PROJECT_API, params);
        sysLogger.debug(String.format("获取projectId的url:%s", getSingleProjectUrl));

        //  创建URI对象
        URI url = null;
        try {
            url = new URI(getSingleProjectUrl);
        } catch (URISyntaxException e) {
            throw new RuntimeException(String.format("使用%s创建URI出错!", getSingleProjectUrl));
        }
        
        /** 2.访问url,获取制定project的信息 */
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> reslut = restTemplate.getForEntity(url, String.class);
        sysLogger.debug(String.format("响应头为:%s,响应体为:%s", reslut.getHeaders(), reslut.getBody()));
        
        /** 3.解析结果 */
        if (reslut.getStatusCode() != HttpStatus.OK ) {
            throw new RuntimeException(String.format("请求%s出错!错误码为:%s", url, reslut.getStatusCode()));
        }
        //  如果响应码是200,说明正常拿到响应结果,解析出projectId返回即可
        JSONObject responBody = JsonUtil.parseObjectToJSONObject(reslut.getBody());
        String projectRepo = responBody.getString("http_url_to_repo");
        String projectId = responBody.getString("id");
        sysLogger.info(String.format("获取到项目:%s的projectId为:%s", projectRepo, projectId));
        
        /** 4.返回projectId */
        return projectId;
    }

需要特别注意,在方法中对配置好的project path进行url编码后,没有直接使用RestTemplate创建get请求获取项目信息,因为实践中发现会出现将本义编码好的如:src%2FHelloWorld.java变为:src%256FHelloWorld.java,具体没有深入RestTemplate源码,所以直接创建URI对象,避免这种情况出现。

4 获取仓库文件内容

api参考:gitlab获取仓库文件内容,需要提供3个参数:

  • private token
  • projectId
  • 文件全路径,需经过url编码,如: main%2Fclass%HelloWorld.java
  • 文件所在分支branch
    整理出具体api的常量类:
private static String GITLAB_FILECONTENT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_ID}/repository/files?private_token=#{PRIVATE_TOKEN}&file_path=#{FILE_PATH}&ref=#{BRANCH_NAME}";

返回的内容json格式如下:

{
  "file_name": "HelloWorld.java",
  "file_path": "main/class/HelloWorld.java",
  "size": 1476,
  "encoding": "base64",
  "content": "IyA9PSBTY2hlbWEgSW5mb3...",
  "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481",
  "ref": "master",
  "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
  "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
  "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
}

特别需要注意的是返回的文件内容是base64编码,拿到结果后需要解码才能获取原始内容。如果想直接获取原始文件内容:获取原始文件内容
所以获取方法如下:

public static String getFileContentFromRepository(String ip,String projectPath,String userName,String password,
            String fileFullPath, String branchName) throws Exception {
        //  校验参数
        Objects.requireNonNull(ip, "参数ip不能为空!");
        Objects.requireNonNull(projectPath, "参数projectPath不能为空!");
        Objects.requireNonNull(userName, "参数userName不能为空!");
        Objects.requireNonNull(password, "参数password不能为空!");
        Objects.requireNonNull(fileFullPath, "参数fileFullPath不能为空!");
        Objects.requireNonNull(branchName, "参数branchName不能为空!");
        
        /** 1.依据用户名、密码获取到用户的privateToken */
        String privateToken = getPrivateTokenByPassword(ip, userName, password);
        
        /** 2.使用privateToken获取项目的projectId */
        String projectId = getProjectId(ip, projectPath, privateToken);
        
        /** 3.使用参数替换形成请求git库中文件内容的uri */
        //  参数准备,存入map
        Map<String, String> params = new HashMap<String, String>(4);
        params.put("REPO_IP", ip);
        params.put("PROJECT_ID", projectId);    
        params.put("PRIVATE_TOKEN", privateToken);  
        params.put("FILE_PATH", fileFullPath);      
        params.put("BRANCH_NAME", branchName);  
        //  使用工具类替换参数
        String reqFileCotnetUri = PlaceholderUtil.anotherReplace(GITLAB_FILECONTENT_API, params);
        sysLogger.debug(String.format("获取文件:%s的uri:%s", fileFullPath, reqFileCotnetUri));       
        
        /** 4.请求gitlab获取文件内容 */
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.getForEntity(reqFileCotnetUri, String.class);
        sysLogger.debug(String.format("响应头为:%s,响应体为:%s", response.getHeaders(), response.getBody()));
        
        /** 5.解析响应结果内容 */
        String body = response.getBody();
        JSONObject jsonBody = JsonUtil.parseObjectToJSONObject(body);
        String fileName = jsonBody.getString("file_name");
        String filePath = jsonBody.getString("file_path");
        String encoding = jsonBody.getString("encoding");
        String content = jsonBody.getString("content");
        String commitId = jsonBody.getString("commit_id");
        String lastCommitId = jsonBody.getString("last_commit_id");

        //  内容已经base64编码,如果需要获取原始文件内容可以参看api:https://docs.gitlab.com/ee/api/repository_files.html#get-raw-file-from-repository
        content = new String(Base64.decode(content), "UTF-8");

        sysLogger.debug(String.format(
                "获取http://%s 上%s项目 %s分支的%s文件,响应信息是:fileName :%s ,filePath:%s , 编码:%s ,内容:%s , commitId:%s ,lastCommitId :%s",
                ip,projectPath, branchName, fileFullPath, fileName, filePath, encoding, content, commitId,
                lastCommitId));

        /** 6.返回指定文件的内容 */
        sysLogger.debug(String.format("解析得到文件内容为:%s", content));
        return content;
    }
    

大致经过如下过程:

  • 使用用户名、密码获取private token
  • 使用private token,project path获取projectId
  • 使用private token,projectId,结合file path、分支branch参数,获取文件base64编码内容,然后解码即可

此外,考虑到项目中实际获取的是配置文件内容,为了剔除不必要的空行、注释行,提供了工具类方法对解码的原始文件内容进行处理:

public static String getCleanFileContentFromRepository(String ip,String projectPath,String userName,String password,
            String fileFullPath, String branchName) throws Exception{
        
        /**  1.获取到原始文件内容 */
        String fileContent = getFileContentFromRepository(ip, projectPath, userName, password, fileFullPath, branchName);
        if (fileContent.isEmpty()) {
            return fileContent;
        }
        
        /** 2.所有行转换为list,并过滤掉空行、#开头的注释行 */
        List<String> list = Arrays.asList(fileContent.split("\n")).stream().filter(a-> (!a.trim().isEmpty()&&!a.trim().startsWith("#"))).collect(Collectors.toList());
        
        /** 3.转为一整行字符串返回 */
        StringBuilder sb = new StringBuilder();
        int size =list.size();
        for (int i = 0; i < size; i++) {
            sb.append(list.get(i).trim());
        }
        return sb.toString();
    }

5 其它相关

5.1 替换参数工具类

对#{}包裹的参数进行替换,代码如下:

public class PlaceholderUtil {
    /** 默认替换形如#{param}的占位符 */
    private static Pattern pattern = Pattern.compile("\\#\\{.*?\\}");
    
    /**
     * 替换字符串中形如#{}的占位符
     * @param src
     * @param parameters
     * @return
     */
    public static String replace(String src, Map<String, Object> parameters) {
        Matcher paraMatcher = pattern.matcher(src);

        // 存储参数名
        String paraName = "";
        String result = new String(src);

        while (paraMatcher.find()) {
            paraName = paraMatcher.group().replaceAll("\\#\\{", "").replaceAll("\\}", "");
            Object objParam = parameters.get(paraName);
            if(objParam!=null){
                result = result.replace(paraMatcher.group(), objParam.toString());
            }
        }
        return result;
    }
    
    
    /**
     * 替换字符串中形如#{}的占位符
     * @param src
     * @param parameters
     * @return
     */
    public static String anotherReplace(String str, Map<String, String> params) {
        Map<String, Object> newParams = new HashMap<>(params);
        return replace(str, newParams);
    }
    
}

5.2 测试类:

public class GitLabAPIUtilsTest {
    
    String repoIp; 
    String privateToken;
    String projectPath1;
    String projectPath2;
    

    String userName;
    String password;
    
    String fileFullPath;
    String branchName;

    @Before
    public void setUp() throws Exception {
        repoIp = "gitlab仓库ip";
        projectPath1 = "acountting/accounting-config-repo";
        projectPath2 = "acountting/csv-filefront-cloud";
        
        userName = "用户名";
        password="密码";
        
        fileFullPath = "/apps/cmup-clearing/0055-account.config";
        branchName = "develop";
    }

    @After
    public void tearDown() throws Exception {
        repoIp = null;
        projectPath1  = null;
        projectPath2  = null;       
        userName = null;
        password = null;        
        fileFullPath = null;
        branchName = null;
    }

    @Test
    public void testGetProjectId() {
        String privateToken = GitLabAPIUtils.getPrivateTokenByPassword(repoIp, userName, password);
        String projectId = GitLabAPIUtils.getProjectId(repoIp, projectPath1, privateToken);
        System.out.println("projectId = " + projectId);
    }

    @Test
    public void testGetPrivateToken() {
        String privateToken = GitLabAPIUtils.getPrivateTokenByPassword(repoIp, userName, password);
        System.out.println("projectId = " + privateToken);
    }
    
    @Test
    public void testGetFileContent() {
        String fileContent;
        try {
            fileContent = GitLabAPIUtils.getCleanFileContentFromRepository(repoIp, projectPath1, userName, password, fileFullPath, branchName);

            System.out.println("fileContent = " + fileContent);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • 这周的主题其实想了很久,打开电脑的时候甚至脑子里都完全是一片空白,所以就先把电脑晾在一边,打开“得到”随意浏览了起...
    麦斯巴斯曼阅读 2,616评论 1 0
  • 那是一双怎样的眼睛 如墨般漆黑 那是双爱笑的眼睛 它看着周围的欢乐 它也笑了 那是双会流泪的眼睛 在深夜 在无人...
    招摇的狗尾巴草阅读 141评论 0 2
  • 互联网时代,风云变幻、暗流涌动,大到信息产业巨头、小到我们每一个普普通通的人都深处其中、受其影响。从农耕时代到工业...
    闫留威_强化班阅读 395评论 4 4