liuyuqi-dellpc 1 month ago
commit
ebb0c1648e
21 changed files with 646 additions and 0 deletions
  1. 27 0
      .dockerignore
  2. 1 0
      .env
  3. 66 0
      .github/workflows/build.yml
  4. 13 0
      .pre-commit-config.yaml
  5. 19 0
      .vscode/launch.json
  6. 40 0
      .vscode/tasks.json
  7. 25 0
      Dockerfile
  8. 38 0
      README.md
  9. 91 0
      client.py
  10. 14 0
      docker-compose.debug.yml
  11. 10 0
      docker-compose.yml
  12. 11 0
      docs/Index.md
  13. 20 0
      fgh/__init__.py
  14. 20 0
      fgh/apps/__init__.py
  15. 18 0
      fgh/apps/config.py
  16. 0 0
      fgh/model/__init__.py
  17. 202 0
      fgh/server.py
  18. 0 0
      fgh/utils/__init__.py
  19. 12 0
      main.py
  20. 3 0
      requirements.txt
  21. 16 0
      scripts/install.sh

+ 27 - 0
.dockerignore

@@ -0,0 +1,27 @@
+**/__pycache__
+**/.venv
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/bin
+**/charts
+**/docker-compose*
+**/compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md

+ 1 - 0
.env

@@ -0,0 +1 @@
+FGIT_HOST=https://ghproxy.org/

+ 66 - 0
.github/workflows/build.yml

@@ -0,0 +1,66 @@
+name: Publish Installers
+
+on:
+  workflow_dispatch: ~
+  push:
+    branches: [master]
+    tags: [v*]
+
+jobs:
+  build:
+    name: Build ${{ matrix.os }} Package
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix: 
+        os: [ubuntu-20.04]
+
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v4
+
+      - name: Set Release Version
+        id: get_version
+        shell: bash
+        run: |
+          echo "::set-output name=hash::$(git rev-parse --short HEAD)"
+          echo "::set-output name=date::$(date +%Y%m%d)"
+          echo "::set-output name=url::$(git remote get-url origin)"
+          
+      - name: Set Up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.9'
+          cache: pip
+          cache-dependency-path: '**/requirements*.txt'
+
+      - name: Install Dependencies
+        run: |
+          python -m pip install --upgrade pip wheel setuptools
+          pip install -r requirements.txt
+          python -m pip install pyinstaller
+
+      - name: Build Package
+        run: |
+          python -m PyInstaller -F -c  --name fgh client.py
+          python -m PyInstaller -F -c  --name fgh-server main.py
+
+      - name: Update to ali oss
+        uses: yizhoumo/setup-ossutil@v1
+        with:
+          endpoint: oss-cn-qingdao.aliyuncs.com
+          access-key-id: ${{ secrets.OSS_KEY_ID }}
+          access-key-secret: ${{ secrets.OSS_KEY_SECRET }}
+          
+      - name: cp files to aliyun
+        run: |
+          ossutil cp -r dist/ oss://yoqi-software/develop/fgh/${{ steps.get_version.outputs.date }}-${{ steps.get_version.outputs.hash }}/
+      
+      - uses: leafney/dingtalk-action@v1
+        if: always()
+        env:
+          DINGTALK_ACCESS_TOKEN: ${{ secrets.DINGTALK_ACCESS_TOKEN }}
+        with:
+          msgtype: link
+          title: 'fgh build success'
+          text: 'please download from aliyun oss. [git.yoqi.me]'
+          msg_url: '${{ steps.get_version.outputs.url }}'

+ 13 - 0
.pre-commit-config.yaml

@@ -0,0 +1,13 @@
+repos:
+  - repo: https://github.com/charliermarsh/ruff-pre-commit
+    # Ruff version.
+    rev: 'v0.0.280'
+    hooks:
+      - id: ruff
+        args: [ --fix, --exit-non-zero-on-fix ]
+
+  - repo: https://github.com/psf/black
+    rev: 23.1.0
+    hooks:
+      - id: black
+

+ 19 - 0
.vscode/launch.json

@@ -0,0 +1,19 @@
+{
+    "configurations": [
+        {
+            "name": "Docker: Python - Flask",
+            "type": "docker",
+            "request": "launch",
+            "preLaunchTask": "docker-run: debug",
+            "python": {
+                "pathMappings": [
+                    {
+                        "localRoot": "${workspaceFolder}",
+                        "remoteRoot": "/app"
+                    }
+                ],
+                "projectType": "flask"
+            }
+        }
+    ]
+}

+ 40 - 0
.vscode/tasks.json

@@ -0,0 +1,40 @@
+{
+	"version": "2.0.0",
+	"tasks": [
+		{
+			"type": "docker-build",
+			"label": "docker-build",
+			"platform": "python",
+			"dockerBuild": {
+				"tag": "fgit:latest",
+				"dockerfile": "${workspaceFolder}/Dockerfile",
+				"context": "${workspaceFolder}",
+				"pull": true
+			}
+		},
+		{
+			"type": "docker-run",
+			"label": "docker-run: debug",
+			"dependsOn": [
+				"docker-build"
+			],
+			"dockerRun": {
+				"env": {
+					"FLASK_APP": "main.py"
+				}
+			},
+			"python": {
+				"args": [
+					"run",
+					"--no-debugger",
+					"--no-reload",
+					"--host",
+					"0.0.0.0",
+					"--port",
+					"5002"
+				],
+				"module": "flask"
+			}
+		}
+	]
+}

+ 25 - 0
Dockerfile

@@ -0,0 +1,25 @@
+# For more information, please refer to https://aka.ms/vscode-docker-python
+FROM python:3.10-slim
+
+EXPOSE 5002
+
+# Keeps Python from generating .pyc files in the container
+ENV PYTHONDONTWRITEBYTECODE=1
+
+# Turns off buffering for easier container logging
+ENV PYTHONUNBUFFERED=1
+
+# Install pip requirements
+COPY requirements.txt .
+RUN python -m pip install -r requirements.txt
+
+WORKDIR /app
+COPY . /app
+
+# Creates a non-root user with an explicit UID and adds permission to access the /app folder
+# For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers
+RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
+USER appuser
+
+# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug
+CMD ["gunicorn", "--bind", "0.0.0.0:5002", "main:app"]

+ 38 - 0
README.md

@@ -0,0 +1,38 @@
+# fgh
+
+加速 clone github 项目,下载 github blob 文件
+
+## Usage
+
+* 服务端:
+
+```
+docker compose up -d
+```
+
+* 客户端:
+
+编辑.env文件,填写服务端地址,执行:
+```
+# clone repo
+fgh git clone https://github.com/xx/yy.git
+fgh git push https://github.com/xx/yy.git
+
+# download file
+fgh wget https://ghproxy.org/https://github.com/microsoft/vscode/archive/refs/tags/1.84.2.zip
+fgh wget https://ghproxy.org/https://raw.githubusercontent.com/microsoft/vscode/main/README.md
+
+fgh curl -O https://ghproxy.org/https://github.com/microsoft/vscode/archive/refs/tags/1.84.2.zip
+fgh curl -O https://ghproxy.org/https://raw.githubusercontent.com/microsoft/vscode/main/README.md
+```
+
+## Reference
+
+- [git clone](https://git-scm.com/docs/git-clone)
+- [hunshcn/gh-proxy](https://github.dev/hunshcn/gh-proxy)
+
+
+## License
+
+* 请遵守 Apache License 2.0 协议,参见 [LICENSE](LICENSE) 文件。
+* 请合规使用,不得用于非法用途。

+ 91 - 0
client.py

@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 12:56:07
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   client enter point
+
+if run :
+    fgh git clone https://github.com/xx/yy.git
+then run :
+    git clone https://user:your_token@{HOST}/https://github.com/your_name/your_private_repo
+
+command:
+    clone, push, pull, commit, add, status, log, diff, branch, checkout, merge, rebase, reset, tag, fetch, remote, init, config, help
+all change it to git command.
+
+if run:
+    fgh wget https://xx.com/yy.zip
+then run:
+    wget https://xx.com/yy.zip
+
+if run:
+    fgh curl -O https://{HOST}/https://xx.com/yy.zip
+then run:
+    curl -O https://{HOST}/https://xx.com/yy.zip
+"""
+
+import os,sys,re
+import argparse
+
+class Client:
+    def __init__(self):
+        self.read_config()
+        self.args = Client.parse_args()
+
+    def read_config(self):
+        import dotenv
+        dotenv.load_dotenv()
+        fgit_host = os.getenv('FGIT_HOST')
+        token = os.getenv('FGIT_TOKEN')
+
+    @staticmethod
+    def parse_args(cls):
+        parser = argparse.ArgumentParser(description='fgit client')
+        parser.add_argument('command', type=str, help='fgit command', 
+                            choices=['clone', 'push', 'pull', 'commit', 'add', 'status', 'log', 'diff', 'branch', 'checkout', 'merge', 'rebase', 'reset', 'tag', 'fetch', 'remote', 'init', 'config', 'help'])
+        parser.add_argument('args', type=str, nargs='*', help='fgit command args')
+        return parser.parse_args()
+
+    def choose_host(self):
+        """choose host"""
+        pass
+
+    def run(self):
+        args = self.parse_args()
+        command = args.command
+        if len(args.args) == 0 and command != 'help':
+            print('Usage: fgit <command> [<args>]')
+            sys.exit(1)
+        if command == 'help':
+            print('Usage: fgit <command> [<args>]')
+            print('Commands:')
+            print('  clone      Clone a repository into a new directory')
+            print('  push       Update remote refs along with associated objects')
+            print('  pull       Fetch from and integrate with another repository or a local branch')
+            print('  commit     Record changes to the repository')
+            print('  add        Add file contents to the index')
+            print('  status     Show the working tree status')
+            print('  log        Show commit logs')
+            print('  diff       Show changes between commits, commit and working tree, etc')
+            print('  branch     List, create, or delete branches')
+            print('  checkout   Switch branches or restore working tree files')
+            print('  merge      Join two or more development histories together')
+            print('  rebase     Reapply commits on top of another base tip')
+            print('  reset      Reset current HEAD to the specified state')
+            print('  tag        Create, list, delete or verify a tag object signed with GPG')
+            print('  fetch      Download objects and refs from another repository')
+            print('  remote     Manage set of tracked repositories')
+            print('  init       Create an empty Git repository or reinitialize an existing one')
+            print('  config     Get and set repository or global options')
+            print('  help       Show help')
+            sys.exit(0)
+        if not re.match(r'^https://github.com', args.args[0]):
+            print('Invalid repo url, only support github.com, for example: https://github.com/xx/yy.git')
+            sys.exit(1)
+        os.system('git ' + command + ' https://{user}:{your_token}@ghproxy.org/' + ' '.join(args.args))
+
+if __name__=='__main__':
+    client = Client()
+    client.run()

+ 14 - 0
docker-compose.debug.yml

@@ -0,0 +1,14 @@
+version: '3.4'
+
+services:
+  fgit:
+    image: fgit
+    build:
+      context: .
+      dockerfile: ./Dockerfile
+    command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m flask run --no-debugger --no-reload --host 0.0.0.0 --port 5002"]
+    ports:
+      - 5002:5002
+      - 5678:5678
+    environment:
+      - FLASK_APP=main.py

+ 10 - 0
docker-compose.yml

@@ -0,0 +1,10 @@
+version: '3.4'
+
+services:
+  fgit:
+    image: fgit
+    build:
+      context: .
+      dockerfile: ./Dockerfile
+    ports:
+      - 5002:5002

+ 11 - 0
docs/Index.md

@@ -0,0 +1,11 @@
+# index
+
+
+## 
+
+https://ghps.cc/
+https://ghproxy.org/
+
+
+
+https://github.dev/hunshcn/gh-proxy

+ 20 - 0
fgh/__init__.py

@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 13:09:13
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   __init__
+"""
+from flask import Flask, jsonify, request, render_template
+from flask_cors import CORS
+from flask_sqlalchemy import SQLAlchemy
+
+def create_app(config_name = "default")-> Flask:
+    app = Flask(__name__)
+    app.config.from_object(config[config_name])
+
+    from .main import main as main_blueprint
+    app.register_blueprint(main_blueprint)
+
+    return app

+ 20 - 0
fgh/apps/__init__.py

@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 13:09:13
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   __init__
+"""
+from flask import Flask, jsonify, request, render_template
+from flask_cors import CORS
+from flask_sqlalchemy import SQLAlchemy
+
+def create_app(config_name = "default")-> Flask:
+    app = Flask(__name__)
+    app.config.from_object(config[config_name])
+
+    from .main import main as main_blueprint
+    app.register_blueprint(main_blueprint)
+
+    return app

+ 18 - 0
fgh/apps/config.py

@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 13:10:31
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   config
+"""
+
+import re
+
+class Config(object):
+    
+    def __init__(self):
+        pass
+
+if __name__ == "__main__":
+    pass

+ 0 - 0
fgh/model/__init__.py


+ 202 - 0
fgh/server.py

@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 12:53:59
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   server
+"""
+
+import re
+
+import requests
+from requests.exceptions import (
+    ChunkedEncodingError,
+    ContentDecodingError, ConnectionError, StreamConsumedError)
+from requests.utils import (
+    stream_decode_response_unicode, iter_slices, CaseInsensitiveDict)
+from flask import Flask, Response, redirect, request
+from urllib3.exceptions import (
+    DecodeError, ReadTimeoutError, ProtocolError)
+from urllib.parse import quote
+
+# config
+# 分支文件使用jsDelivr镜像的开关,0为关闭,默认关闭
+jsdelivr = 0
+size_limit = 1024 * 1024 * 1024 * 999  # 允许的文件大小,默认999GB,相当于无限制了 https://github.com/hunshcn/gh-proxy/issues/8
+
+"""
+  先生效白名单再匹配黑名单,pass_list 匹配到的会直接302到jsdelivr而忽略设置
+  生效顺序 白->黑->pass,可以前往https://github.com/hunshcn/gh-proxy/issues/41 查看示例
+  每个规则一行,可以封禁某个用户的所有仓库,也可以封禁某个用户的特定仓库,下方用黑名单示例,白名单同理
+  user1 # 封禁user1的所有仓库
+  user1/repo1 # 封禁user1的repo1
+  */repo1 # 封禁所有叫做repo1的仓库
+"""
+white_list = '''
+'''
+black_list = '''
+'''
+pass_list = '''
+'''
+
+ASSET_URL = 'https://hunshcn.github.io/gh-proxy'  # 主页
+
+white_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in white_list.split('\n') if i]
+black_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in black_list.split('\n') if i]
+pass_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in pass_list.split('\n') if i]
+app = Flask(__name__)
+CHUNK_SIZE = 1024 * 10
+index_html = requests.get(ASSET_URL, timeout=10).text
+icon_r = requests.get(ASSET_URL + '/favicon.ico', timeout=10).content
+exp1 = re.compile(r'^(?:https?://)?github\.com/(?P<author>.+?)/(?P<repo>.+?)/(?:releases|archive)/.*$')
+exp2 = re.compile(r'^(?:https?://)?github\.com/(?P<author>.+?)/(?P<repo>.+?)/(?:blob|raw)/.*$')
+exp3 = re.compile(r'^(?:https?://)?github\.com/(?P<author>.+?)/(?P<repo>.+?)/(?:info|git-).*$')
+exp4 = re.compile(r'^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?P<author>.+?)/(?P<repo>.+?)/.+?/.+$')
+exp5 = re.compile(r'^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?P<author>.+?)/.+?/.+$')
+
+requests.sessions.default_headers = lambda: CaseInsensitiveDict()
+
+
+@app.route('/')
+def index():
+    
+    if 'q' in request.args:
+        return redirect('/' + request.args.get('q'))
+    return index_html
+
+
+@app.route('/favicon.ico')
+def icon():
+    return Response(icon_r, content_type='image/vnd.microsoft.icon')
+
+
+def iter_content(self, chunk_size=1, decode_unicode=False):
+    """rewrite requests function, set decode_content with False"""
+
+    def generate():
+        # Special case for urllib3.
+        if hasattr(self.raw, 'stream'):
+            try:
+                for chunk in self.raw.stream(chunk_size, decode_content=False):
+                    yield chunk
+            except ProtocolError as e:
+                raise ChunkedEncodingError(e)
+            except DecodeError as e:
+                raise ContentDecodingError(e)
+            except ReadTimeoutError as e:
+                raise ConnectionError(e)
+        else:
+            # Standard file-like object.
+            while True:
+                chunk = self.raw.read(chunk_size)
+                if not chunk:
+                    break
+                yield chunk
+
+        self._content_consumed = True
+
+    if self._content_consumed and isinstance(self._content, bool):
+        raise StreamConsumedError()
+    elif chunk_size is not None and not isinstance(chunk_size, int):
+        raise TypeError("chunk_size must be an int, it is instead a %s." % type(chunk_size))
+    # simulate reading small chunks of the content
+    reused_chunks = iter_slices(self._content, chunk_size)
+
+    stream_chunks = generate()
+
+    chunks = reused_chunks if self._content_consumed else stream_chunks
+
+    if decode_unicode:
+        chunks = stream_decode_response_unicode(chunks, self)
+
+    return chunks
+
+
+def check_url(u):
+    for exp in (exp1, exp2, exp3, exp4, exp5):
+        m = exp.match(u)
+        if m:
+            return m
+    return False
+
+
+@app.route('/<path:u>', methods=['GET', 'POST'])
+def handler(u):
+    u = u if u.startswith('http') else 'https://' + u
+    if u.rfind('://', 3, 9) == -1:
+        u = u.replace('s:/', 's://', 1)  # uwsgi会将//传递为/
+    pass_by = False
+    m = check_url(u)
+    if m:
+        m = tuple(m.groups())
+        if white_list:
+            for i in white_list:
+                if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]:
+                    break
+            else:
+                return Response('Forbidden by white list.', status=403)
+        for i in black_list:
+            if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]:
+                return Response('Forbidden by black list.', status=403)
+        for i in pass_list:
+            if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]:
+                pass_by = True
+                break
+    else:
+        return Response('Invalid input.', status=403)
+
+    if (jsdelivr or pass_by) and exp2.match(u):
+        u = u.replace('/blob/', '@', 1).replace('github.com', 'cdn.jsdelivr.net/gh', 1)
+        return redirect(u)
+    elif (jsdelivr or pass_by) and exp4.match(u):
+        u = re.sub(r'(\.com/.*?/.+?)/(.+?/)', r'\1@\2', u, 1)
+        _u = u.replace('raw.githubusercontent.com', 'cdn.jsdelivr.net/gh', 1)
+        u = u.replace('raw.github.com', 'cdn.jsdelivr.net/gh', 1) if _u == u else _u
+        return redirect(u)
+    else:
+        if exp2.match(u):
+            u = u.replace('/blob/', '/raw/', 1)
+        if pass_by:
+            url = u + request.url.replace(request.base_url, '', 1)
+            if url.startswith('https:/') and not url.startswith('https://'):
+                url = 'https://' + url[7:]
+            return redirect(url)
+        u = quote(u, safe='/:')
+        return proxy(u)
+
+
+def proxy(u, allow_redirects=False):
+    headers = {}
+    r_headers = dict(request.headers)
+    if 'Host' in r_headers:
+        r_headers.pop('Host')
+    try:
+        url = u + request.url.replace(request.base_url, '', 1)
+        if url.startswith('https:/') and not url.startswith('https://'):
+            url = 'https://' + url[7:]
+        r = requests.request(method=request.method, url=url, data=request.data, headers=r_headers, stream=True, allow_redirects=allow_redirects)
+        headers = dict(r.headers)
+
+        if 'Content-length' in r.headers and int(r.headers['Content-length']) > size_limit:
+            return redirect(u + request.url.replace(request.base_url, '', 1))
+
+        def generate():
+            for chunk in iter_content(r, chunk_size=CHUNK_SIZE):
+                yield chunk
+
+        if 'Location' in r.headers:
+            _location = r.headers.get('Location')
+            if check_url(_location):
+                headers['Location'] = '/' + _location
+            else:
+                return proxy(_location, True)
+
+        return Response(generate(), headers=headers, status=r.status_code)
+    except Exception as e:
+        headers['content-type'] = 'text/html; charset=UTF-8'
+        return Response('server error ' + str(e), status=500, headers=headers)
+
+
+if __name__ == '__main__':
+    app.run(host="127.0.0.1", port=8080, debug=True)

+ 0 - 0
fgh/utils/__init__.py


+ 12 - 0
main.py

@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/04/09 12:48:43
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   enter point
+"""
+from fgh.server import app
+
+if __name__=='__main__':
+    app.run(host="127.0.0.1", port=8080, debug=True)

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
+flask==3.0.0
+gunicorn==20.1.0

+ 16 - 0
scripts/install.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+# @Contact :   liuyuqi.gov@msn.cn
+# @Time    :   2024/04/09 13:38:51
+# @License :   (C)Copyright 2022 liuyuqi.
+# @Desc    :   install fgh to os system
+###############################################################################
+
+mkdir -p /opt/fgh
+wget https://fileshare.yoqi.me/fgh/fgh -O /opt/fgh/fgh
+chmod +x /opt/fgh/fgh
+
+ln -s /opt/fgh/fgh /usr/local/bin/fgh
+echo "export PATH=$PATH:/opt/fgh" >> /etc/profile
+source /etc/profile
+
+echo "fgh installed successfully"