From 7b7d5f2f4966ba5bdc4622e22dda761a6d8e3ffc Mon Sep 17 00:00:00 2001 From: Shruti Chaturvedi Date: Mon, 12 Dec 2022 21:08:43 +0530 Subject: [PATCH 01/11] Integrate Uffizzi --- .github/workflows/uffizzi-build.yml | 98 +++++++++++++++++++++++++++ .github/workflows/uffizzi-preview.yml | 88 ++++++++++++++++++++++++ docker-compose.uffizzi.yml | 36 ++++++++++ 3 files changed, 222 insertions(+) create mode 100644 .github/workflows/uffizzi-build.yml create mode 100644 .github/workflows/uffizzi-preview.yml create mode 100644 docker-compose.uffizzi.yml diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml new file mode 100644 index 000000000..8786952c6 --- /dev/null +++ b/.github/workflows/uffizzi-build.yml @@ -0,0 +1,98 @@ +name: Build PR Image +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +jobs: + build-answer: + name: Build and push `Answer` + runs-on: ubuntu-latest + outputs: + tags: ${{ steps.meta.outputs.tags }} + if: ${{ github.event.action != 'closed' }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Generate UUID image name + id: uuid + run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: registry.uffizzi.com/${{ env.UUID_WORKER }} + tags: | + type=raw,value=60d + + - name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + cache-from: type=gha + cache-to: type=gha, mode=max + + render-compose-file: + name: Render Docker Compose File + # Pass output of this workflow to another triggered by `workflow_run` event. + runs-on: ubuntu-latest + needs: + - build-answer + outputs: + compose-file-cache-key: ${{ steps.hash.outputs.hash }} + steps: + - name: Checkout git repo + uses: actions/checkout@v3 + - name: Render Compose File + run: | + ANSWER_IMAGE=${{ needs.build-answer.outputs.tags }} + export ANSWER_IMAGE + export UFFIZZI_URL=\$UFFIZZI_URL + # Render simple template from environment variables. + envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml + cat docker-compose.rendered.yml + - name: Upload Rendered Compose File as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: docker-compose.rendered.yml + retention-days: 2 + - name: Serialize PR Event to File + run: | + cat << EOF > event.json + ${{ toJSON(github.event) }} + + EOF + - name: Upload PR Event as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: event.json + retention-days: 2 + + delete-preview: + name: Call for Preview Deletion + runs-on: ubuntu-latest + if: ${{ github.event.action == 'closed' }} + steps: + # If this PR is closing, we will not render a compose file nor pass it to the next workflow. + - name: Serialize PR Event to File + run: | + cat << EOF > event.json + ${{ toJSON(github.event) }} + + EOF + - name: Upload PR Event as Artifact + uses: actions/upload-artifact@v3 + with: + name: preview-spec + path: event.json + retention-days: 2 diff --git a/.github/workflows/uffizzi-preview.yml b/.github/workflows/uffizzi-preview.yml new file mode 100644 index 000000000..f79097414 --- /dev/null +++ b/.github/workflows/uffizzi-preview.yml @@ -0,0 +1,88 @@ +name: Deploy Uffizzi Preview + +on: + workflow_run: + workflows: + - "Build PR Image" + types: + - completed + + +jobs: + cache-compose-file: + name: Cache Compose File + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + outputs: + compose-file-cache-key: ${{ env.HASH }} + pr-number: ${{ env.PR_NUMBER }} + steps: + - name: 'Download artifacts' + # Fetch output (zip archive) from the workflow run that triggered this workflow. + uses: actions/github-script@v6 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "preview-spec" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data)); + + - name: 'Unzip artifact' + run: unzip preview-spec.zip + - name: Read Event into ENV + run: | + echo 'EVENT_JSON<> $GITHUB_ENV + cat event.json >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + + - name: Hash Rendered Compose File + id: hash + # If the previous workflow was triggered by a PR close event, we will not have a compose file artifact. + if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV + - name: Cache Rendered Compose File + if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + uses: actions/cache@v3 + with: + path: docker-compose.rendered.yml + key: ${{ env.HASH }} + + - name: Read PR Number From Event Object + id: pr + run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV + - name: DEBUG - Print Job Outputs + if: ${{ runner.debug }} + run: | + echo "PR number: ${{ env.PR_NUMBER }}" + echo "Compose file hash: ${{ env.HASH }}" + cat event.json + + deploy-uffizzi-preview: + name: Use Remote Workflow to Preview on Uffizzi + needs: + - cache-compose-file + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2 + with: + # If this workflow was triggered by a PR close event, cache-key will be an empty string + # and this reusable workflow will delete the preview deployment. + compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }} + compose-file-cache-path: docker-compose.rendered.yml + server: https://app.uffizzi.com + pr-number: ${{ needs.cache-compose-file.outputs.pr-number }} + permissions: + contents: read + pull-requests: write + id-token: write diff --git a/docker-compose.uffizzi.yml b/docker-compose.uffizzi.yml new file mode 100644 index 000000000..f26d2bdce --- /dev/null +++ b/docker-compose.uffizzi.yml @@ -0,0 +1,36 @@ +version: "3" + +# uffizzi integration +x-uffizzi: + ingress: + service: answer + port: 80 + +services: + + answer: + image: "${ANSWER_IMAGE}" + volumes: + - answer-data:/data + deploy: + resources: + limits: + memory: 4000M + + mysql: + image: mysql:latest + environment: + - MYSQL_DATABASE=answer + - MYSQL_ROOT_PASSWORD=password + - MYSQL_USER=mysql + - MYSQL_PASSWORD=mysql + volumes: + - sql_data:/var/lib/mysql + deploy: + resources: + limits: + memory: 500M + +volumes: + answer-data: + sql_data: From a7a8cbf35cc56e4497abd395fe33bceaa7cf6bfa Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 6 Feb 2023 10:55:09 +0800 Subject: [PATCH 02/11] docs(i18n): Update i18n files --- i18n/en_US.yaml | 210 ++++++++++++++++++++++++------------------------ 1 file changed, 106 insertions(+), 104 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 9d1c1b20a..dfa484c0b 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -3,242 +3,244 @@ backend: base: success: - other: "Success." + other: Success. unknown: - other: "Unknown error." + other: Unknown error. request_format_error: - other: "Request format is not valid." + other: Request format is not valid. unauthorized_error: - other: "Unauthorized." + other: Unauthorized. database_error: - other: "Data server error." - + other: Data server error. role: name: user: - other: "User" + other: User admin: - other: "Admin" + other: Admin moderator: - other: "Moderator" + other: Moderator description: user: - other: "Default with no special access." + other: Default with no special access. admin: - other: "Have the full power to access the site." + other: Have the full power to access the site. moderator: - other: "Has access to all posts except admin settings." - + other: Has access to all posts except admin settings. email: - other: "Email" + other: Email password: - other: "Password" - - email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - + other: Password + email_or_password_wrong_error: + other: Email and password do not match. error: admin: - email_or_password_wrong: *email_or_password_wrong + email_or_password_wrong: + other: Email and password do not match. answer: not_found: - other: "Answer do not found." + other: Answer do not found. cannot_deleted: - other: "No permission to delete." + other: No permission to delete. cannot_update: - other: "No permission to update." + other: No permission to update. comment: edit_without_permission: - other: "Comment are not allowed to edit." + other: Comment are not allowed to edit. not_found: - other: "Comment not found." + other: Comment not found. cannot_edit_after_deadline: - other: "The comment time has been too long to modify." + other: The comment time has been too long to modify. email: duplicate: - other: "Email already exists." + other: Email already exists. need_to_be_verified: - other: "Email should be verified." + other: Email should be verified. verify_url_expired: - other: "Email verified URL has expired, please resend the email." + other: Email verified URL has expired, please resend the email. lang: not_found: - other: "Language file not found." + other: Language file not found. object: captcha_verification_failed: - other: "Captcha wrong." + other: Captcha wrong. disallow_follow: - other: "You are not allowed to follow." + other: You are not allowed to follow. disallow_vote: - other: "You are not allowed to vote." + other: You are not allowed to vote. disallow_vote_your_self: - other: "You can't vote for your own post." + other: You can't vote for your own post. not_found: - other: "Object not found." + other: Object not found. verification_failed: - other: "Verification failed." + other: Verification failed. email_or_password_incorrect: - other: "Email and password do not match." + other: Email and password do not match. old_password_verification_failed: - other: "The old password verification failed" + other: The old password verification failed new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." + other: The new password is the same as the previous one. question: not_found: - other: "Question not found." + other: Question not found. cannot_deleted: - other: "No permission to delete." + other: No permission to delete. cannot_close: - other: "No permission to close." + other: No permission to close. cannot_update: - other: "No permission to update." + other: No permission to update. rank: fail_to_meet_the_condition: - other: "Rank fail to meet the condition." + other: Rank fail to meet the condition. report: handle_failed: - other: "Report handle failed." + other: Report handle failed. not_found: - other: "Report not found." + other: Report not found. tag: not_found: - other: "Tag not found." + other: Tag not found. recommend_tag_not_found: - other: "Recommend Tag is not exist." + other: Recommend Tag is not exist. recommend_tag_enter: - other: "Please enter at least one required tag." + other: Please enter at least one required tag. not_contain_synonym_tags: - other: "Should not contain synonym tags." + other: Should not contain synonym tags. cannot_update: - other: "No permission to update." + other: No permission to update. cannot_set_synonym_as_itself: - other: "You cannot set the synonym of the current tag as itself." + other: You cannot set the synonym of the current tag as itself. smtp: config_from_name_cannot_be_email: - other: "The From Name cannot be a email address." + other: The From Name cannot be a email address. theme: not_found: - other: "Theme not found." + other: Theme not found. revision: review_underway: - other: "Can't edit currently, there is a version in the review queue." + other: Can't edit currently, there is a version in the review queue. no_permission: - other: "No permission to Revision." + other: No permission to Revision. user: email_or_password_wrong: - other: *email_or_password_wrong + other: + other: Email and password do not match. not_found: - other: "User not found." + other: User not found. suspended: - other: "User has been suspended." + other: User has been suspended. username_invalid: - other: "Username is invalid." + other: Username is invalid. username_duplicate: - other: "Username is already in use." + other: Username is already in use. set_avatar: - other: "Avatar set failed." + other: Avatar set failed. cannot_update_your_role: - other: "You cannot modify your role." + other: You cannot modify your role. not_allowed_registration: - other: "Currently the site is not open for registration" + other: Currently the site is not open for registration config: read_config_failed: - other: "Read config failed" + other: Read config failed database: connection_failed: - other: "Database connection failed" + other: Database connection failed create_table_failed: - other: "Create table failed" + other: Create table failed install: create_config_failed: - other: "Can't create the config.yaml file." + other: Can't create the config.yaml file. report: spam: name: - other: "spam" + other: spam desc: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + other: This post is an advertisement, or vandalism. It is not useful or relevant + to the current topic. rude: name: - other: "rude or abusive" + other: rude or abusive desc: - other: "A reasonable person would find this content inappropriate for respectful discourse." + other: A reasonable person would find this content inappropriate for respectful + discourse. duplicate: name: - other: "a duplicate" + other: a duplicate desc: - other: "This question has been asked before and already has an answer." + other: This question has been asked before and already has an answer. not_answer: name: - other: "not an answer" + other: not an answer desc: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + other: This was posted as an answer, but it does not attempt to answer the + question. It should possibly be an edit, a comment, another question, + or deleted altogether. not_need: name: - other: "no longer needed" + other: no longer needed desc: - other: "This comment is outdated, conversational or not relevant to this post." + other: This comment is outdated, conversational or not relevant to this post. other: name: - other: "something else" + other: something else desc: - other: "This post requires staff attention for another reason not listed above." - + other: This post requires staff attention for another reason not listed above. question: close: duplicate: name: - other: "spam" + other: spam desc: - other: "This question has been asked before and already has an answer." + other: This question has been asked before and already has an answer. guideline: name: - other: "a community-specific reason" + other: a community-specific reason desc: - other: "This question doesn't meet a community guideline." + other: This question doesn't meet a community guideline. multiple: name: - other: "needs details or clarity" + other: needs details or clarity desc: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: This question currently includes multiple questions in one. It should + focus on one problem only. other: name: - other: "something else" + other: something else desc: - other: "This post requires another reason not listed above." + other: This post requires another reason not listed above. operation_type: asked: - other: "asked" + other: asked answered: - other: "answered" + other: answered modified: - other: "modified" + other: modified notification: action: update_question: - other: "updated question" + other: updated question answer_the_question: - other: "answered question" + other: answered question update_answer: - other: "updated answer" + other: updated answer accept_answer: - other: "accepted answer" + other: accepted answer comment_question: - other: "commented question" + other: commented question comment_answer: - other: "commented answer" + other: commented answer reply_to_you: - other: "replied to you" + other: replied to you mention_you: - other: "mentioned you" + other: mentioned you your_question_is_closed: - other: "Your question has been closed" + other: Your question has been closed your_question_was_deleted: - other: "Your question has been deleted" + other: Your question has been deleted your_answer_was_deleted: - other: "Your answer has been deleted" + other: Your answer has been deleted your_comment_was_deleted: - other: "Your comment has been deleted" + other: Your comment has been deleted # The following fields are used for interface presentation(Front-end) ui: From 2d8457419d5216dfee774310bc69c24f7488dbbe Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 6 Feb 2023 13:43:13 +0800 Subject: [PATCH 03/11] update github action dockerhub img --- .github/workflows/build_dockerhub_img.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_dockerhub_img.yml b/.github/workflows/build_dockerhub_img.yml index 07c5a0a9d..689a8e016 100644 --- a/.github/workflows/build_dockerhub_img.yml +++ b/.github/workflows/build_dockerhub_img.yml @@ -24,6 +24,9 @@ jobs: uses: docker/setup-buildx-action@v2 with: platforms: linux/amd64,linux/arm64 + version: latest + endpoint: builders + driver: docker - name: Login to DockerHub uses: docker/login-action@v1 From d798b7be44a88cb060808c775ae97e52de4bc2c8 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 6 Feb 2023 14:11:35 +0800 Subject: [PATCH 04/11] update action docker build --- .github/workflows/build_dockerhub_img.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_dockerhub_img.yml b/.github/workflows/build_dockerhub_img.yml index 689a8e016..a59087192 100644 --- a/.github/workflows/build_dockerhub_img.yml +++ b/.github/workflows/build_dockerhub_img.yml @@ -50,7 +50,7 @@ jobs: - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile From c21f632c96fb17922c1e51e0ade56d967d69125c Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:14:56 +0800 Subject: [PATCH 05/11] update build docker img --- .github/workflows/build_dockerhub_img.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_dockerhub_img.yml b/.github/workflows/build_dockerhub_img.yml index a59087192..2487c2f0f 100644 --- a/.github/workflows/build_dockerhub_img.yml +++ b/.github/workflows/build_dockerhub_img.yml @@ -49,12 +49,22 @@ jobs: - - name: Build and push + - name: Build amd64 uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Build arm64 + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 600a724e91bd19acc2e7cb4b1249a18640c7784f Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:38:31 +0800 Subject: [PATCH 06/11] update go mod sum --- go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/go.sum b/go.sum index a84e52f35..2a19ee7f3 100644 --- a/go.sum +++ b/go.sum @@ -38,7 +38,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= -gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= From fc396ef98754aa87b60da2ec2754ab0e16d9200c Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 6 Feb 2023 15:51:44 +0800 Subject: [PATCH 07/11] fix(i18n): Resolve an issue that causes unexpected errors when translation files for unsupported languages are not properly initialized. --- internal/base/translator/provider.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index 3d903a14a..bff574863 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -8,6 +8,7 @@ import ( "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" "gopkg.in/yaml.v3" ) @@ -68,12 +69,14 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) { content, err := yaml.Marshal(translation) if err != nil { - return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err) + log.Debugf("marshal translation content failed: %s %s", file.Name(), err) + continue } // add translator use backend translation if err = myTran.AddTranslator(content, file.Name()); err != nil { - return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err) + log.Debugf("add translator failed: %s %s", file.Name(), err) + continue } } GlobalTrans = myTran.GlobalTrans From 808a2ebc1288386940147543cd2fdd629c184ace Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 6 Feb 2023 17:42:42 +0800 Subject: [PATCH 08/11] update go mod --- go.mod | 3 +++ go.sum | 1 + 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 1db48ddb7..be249e797 100644 --- a/go.mod +++ b/go.mod @@ -136,3 +136,6 @@ require ( modernc.org/token v1.0.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +// github action runner Sometimes it will time out. +replace gitee.com/travelliu/dm v1.8.11192 => github.com/aichy126/dm v1.8.11192 diff --git a/go.sum b/go.sum index 2a19ee7f3..3a2504b47 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/aichy126/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= From 964e6bd2d46a75690823403ed4d2745784be5607 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 6 Feb 2023 19:04:05 +0800 Subject: [PATCH 09/11] fix(upload): add image file check --- i18n/en_US.yaml | 3 +++ internal/base/reason/reason.go | 1 + internal/service/uploader/upload.go | 39 ++++++++++++++++------------- pkg/checker/file_type.go | 29 +++++++++++++++++++++ 4 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 pkg/checker/file_type.go diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 9d1c1b20a..b58a870f8 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -152,6 +152,9 @@ backend: install: create_config_failed: other: "Can't create the config.yaml file." + upload: + unsupported_file_format: + other: Unsupported file format. report: spam: name: diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index af4b5b719..bc726d1fd 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -54,6 +54,7 @@ const ( InstallConfigFailed = "error.install.create_config_failed" SiteInfoNotFound = "error.site_info.not_found" UploadFileSourceUnsupported = "error.upload.source_unsupported" + UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format" RecommendTagNotExist = "error.tag.recommend_tag_not_found" RecommendTagEnter = "error.tag.recommend_tag_enter" RevisionReviewUnderway = "error.revision.review_underway" diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 3a3df6e80..f438e7ddb 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -11,10 +11,10 @@ import ( "path/filepath" "strings" - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo_common" + "github.com/answerdev/answer/pkg/checker" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" "github.com/disintegration/imaging" @@ -40,10 +40,10 @@ var ( ".jpg": imaging.JPEG, ".jpeg": imaging.JPEG, ".png": imaging.PNG, - ".gif": imaging.GIF, - ".tif": imaging.TIFF, - ".tiff": imaging.TIFF, - ".bmp": imaging.BMP, + //".gif": imaging.GIF, + //".tif": imaging.TIFF, + //".tiff": imaging.TIFF, + //".bmp": imaging.BMP, } ) @@ -74,13 +74,11 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024) _, file, err := ctx.Request.FormFile("file") if err != nil { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } fileExt := strings.ToLower(path.Ext(file.Filename)) if _, ok := FormatExts[fileExt]; !ok { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) @@ -147,13 +145,11 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) ( ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) _, file, err := ctx.Request.FormFile("file") if err != nil { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } fileExt := strings.ToLower(path.Ext(file.Filename)) if _, ok := FormatExts[fileExt]; !ok { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) @@ -167,14 +163,12 @@ func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) ( ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) _, file, err := ctx.Request.FormFile("file") if err != nil { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } fileExt := strings.ToLower(path.Ext(file.Filename)) _, ok := FormatExts[fileExt] if !ok && fileExt != ".ico" { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) @@ -192,6 +186,17 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } + + src, err := file.Open() + if err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + defer src.Close() + + if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) { + return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat) + } + url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } diff --git a/pkg/checker/file_type.go b/pkg/checker/file_type.go new file mode 100644 index 000000000..a5918a446 --- /dev/null +++ b/pkg/checker/file_type.go @@ -0,0 +1,29 @@ +package checker + +import ( + "image/jpeg" + "image/png" + "io" + "strings" +) + +// IsSupportedImageFile currently answers support image type is `image/jpeg,image/jpg,image/png` +func IsSupportedImageFile(file io.Reader, ext string) bool { + ext = strings.TrimPrefix(ext, ".") + var err error + switch strings.ToUpper(ext) { + case "JPG", "JPEG": + _, err = jpeg.Decode(file) + case "PNG": + _, err = png.Decode(file) + case "ICO": + // TODO: There is currently no good Golang library to parse whether the image is in ico format. + return true + default: + return false + } + if err != nil { + return false + } + return true +} From 4f8771cdff0f37ed26da0f787fc7ab671fc28926 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:46:52 +0800 Subject: [PATCH 10/11] update renderAutoLink --- pkg/converter/markdown.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index 4f4a3c41a..d61d79f94 100644 --- a/pkg/converter/markdown.go +++ b/pkg/converter/markdown.go @@ -57,6 +57,8 @@ func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegis reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) reg.Register(ast.KindRawHTML, r.renderRawHTML) reg.Register(ast.KindLink, r.renderLink) + reg.Register(ast.KindAutoLink, r.renderAutoLink) + } func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { @@ -90,6 +92,7 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, } func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Link) if entering && r.renderLinkIsUrl(string(n.Destination)) { _, _ = w.WriteString("') + } else { + _, _ = w.WriteString(`">`) + } + _, _ = w.Write(util.EscapeHTML(label)) + _, _ = w.WriteString(``) + return ast.WalkContinue, nil +} + func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool { return govalidator.IsURL(verifyUrl) } From 860b1a3bd8cfaa8827e6e6f50ab1d98fa4c2c816 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:55:31 +0800 Subject: [PATCH 11/11] update img header --- internal/base/middleware/avatar.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 66a72f68e..d38e76c9b 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "path" "path/filepath" "strings" @@ -53,6 +54,8 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { ctx.Next() return } + ext := strings.ToLower(path.Ext(filePath)[1:]) + ctx.Header("content-type", fmt.Sprintf("image/%s", ext)) _, err = ctx.Writer.WriteString(string(avatarfile)) if err != nil { log.Error(err) @@ -60,6 +63,17 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { ctx.Abort() return + } else { + uUrl, err := url.Parse(u) + if err != nil { + ctx.Next() + return + } + _, urlfileName := filepath.Split(uUrl.Path) + uploadPath := am.serviceConfig.UploadPath + filePath := fmt.Sprintf("%s/%s", uploadPath, urlfileName) + ext := strings.ToLower(path.Ext(filePath)[1:]) + ctx.Header("content-type", fmt.Sprintf("image/%s", ext)) } ctx.Next() }