├── .circleci └── config.yml ├── .config_file.json ├── .gitignore ├── README.md ├── dist └── notion-exported │ ├── NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733.md │ ├── NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733 │ ├── Any notion page here be73b9e8e0924786a91369c4c7c79bbe.md │ ├── Page Elements fdc4e78fe4c14e88993cfae1b2e6b016.md │ └── Page Template 4c61829a8bf848238b155d8c46321bc9.md │ ├── NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34.md │ └── NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34 │ ├── Hexo Integration Post 01f2281aa8604215a3168ba063d72705.md │ ├── Hexo Integration Post 01f2281aa8604215a3168ba063d72705 │ ├── Hexo page - Chinese Test 2ac62b5b859246bd9b4d4ff50d223a77.md │ ├── Hexo page - Elements Showcase 1cdcf1ab8c0d44fc83a2728f932ca1ba.md │ ├── Hexo page - Japanese Test 975cac84c85249698a1745868ec50616.md │ ├── Hexo page - Link Post 9371556035d3412f85ea0f88d8f19b04.md │ ├── Hexo page - NotionDown 6301820e74974612966ef5638a5b4e8a.md │ └── Hexo page - draft post ef436e83cd1f4d60b636950a177f5f7b.md │ ├── MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b.md │ ├── MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b │ ├── MarkDown FileLocate Test R2 88187dd965a042cb97e6eaee6bc34d0b.md │ ├── MarkDown FileLocate Test R3 a2d3447bac4b4f379cb8b0cd744b5956.md │ └── MarkDown FileName Test 5862241766b548f9a6d47f446223703f.md │ ├── MarkDown Test Page - SPA 619ae183133840ad9ca2e51b2f00f0bd.md │ ├── MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4.md │ ├── MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4 │ ├── Table f802ebd06ae241c989da3c1c66042ea0.csv │ ├── Table f802ebd06ae241c989da3c1c66042ea0 │ │ ├── 测试 0bf98d3a244c4e7cae082693ddb01825.md │ │ └── 测试_NULL 0d231330e5314eacb145e8e5f1dadad4.md │ └── kiminonaha_tenkinoko_2.jpg │ ├── NotionDown CN-EN Concat Format 4be67947e23843ad81b007bd2ee41c17.md │ ├── NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448.md │ ├── NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448 │ ├── Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7.csv │ ├── Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7 │ │ ├── blog_url 7148bb3511c54f1cb1fb94a9a480babf.md │ │ ├── channels f669a7efd2f647a188d7aea7fe9cf14a.md │ │ ├── config_file e24099ca0dec47999323140543add9f5.md │ │ ├── debuggable a65ea92ffad34abdb93a06932668e13c.md │ │ ├── page_titles 9a0c6bc317444b85bf8777102895b87b.md │ │ └── token_v2 c53bb2c8055949848ee0574dc7cb2ee9.md │ ├── Untitled Database 8818f00cbc684caaa89eb19ac003d4e6.csv │ └── Untitled Database 8818f00cbc684caaa89eb19ac003d4e6 │ │ ├── Category c99ecf42575740c2887675593dcce017.md │ │ ├── Date 8f5afa6814074f688b42eb3790aa4f6f.md │ │ ├── FileLocate 244eda6d681246f58f89905827ddada5.md │ │ ├── FileName 725e6562ca304f47b009eb756ce9e93a.md │ │ ├── Published e310c539ddf74b8b9ec1e2da54961712.md │ │ ├── Tag 9762b5a017cf49e8886391c4847d8108.md │ │ └── Title b2628f72f7684966bb18d5cd0fd99346.md │ ├── NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09.md │ ├── NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09 │ ├── Untitled 1.png │ ├── Untitled 2.png │ └── Untitled.png │ ├── NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085.md │ ├── NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085 │ ├── Sample_Image 1.jpg │ ├── Sample_Image 2.jpg │ ├── Sample_Image.jpg │ └── my_caption.jpg │ ├── NotionDown Nested List ee35f5e58d834865bc38f4e01531e98f.md │ ├── NotionDown Obfuscated Blocks 25959a72e55041d6aed69f90226fa45c.md │ ├── NotionDown Obfuscated Blocks 25959a72e55041d6aed69f90226fa45c │ └── mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg │ ├── NotionDown Properties 7c43bf2594954ec9816eede682d8e774.md │ ├── NotionDown Properties 7c43bf2594954ec9816eede682d8e774 │ ├── Untitled Database 9169514bc3d14895b9d04c303b43e90c.csv │ └── Untitled Database 9169514bc3d14895b9d04c303b43e90c │ │ ├── Category f2c5fb07a7714a4b8d2666595cf8f80f.md │ │ ├── Date 0d0cb3cfcf4a4eeab72d681d28f07e51.md │ │ ├── FileLocate 8c09075d24dd410fb1b792d46fc0e9fd.md │ │ ├── FileName 7705b604933449d69bea9e3df0373ba2.md │ │ ├── Published 9b231c2452a648819bc0db771885e910.md │ │ ├── Tag 72f6b19212bb40b5aa42f45be0f97a38.md │ │ └── Title 98ac1e50846848a4b053d3870a2bde19.md │ ├── NotionDown Pullquote Blocks 5e84918760af4ff78fd8ade884955189.md │ ├── NotionDown Pullquote Blocks 5e84918760af4ff78fd8ade884955189 │ └── kiminonaha_tenkinoko_2.jpg │ ├── NotionDown README d3463f3d398743879d663caf87efa029.md │ ├── NotionDown README d3463f3d398743879d663caf87efa029 │ └── NotionDown.png │ ├── NotionDown ShortCode daa1568f28a64afebdc57c43b1401926.md │ ├── NotionDown Spelling Inspect a6721bfa57da451d8a328fa1f3418969.md │ ├── NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818.md │ └── NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818 │ ├── Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52.csv │ └── Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52 │ ├── Untitled c8a900b2bb334ff5a7d9bdd0d3dc2a27.md │ ├── 测试 919e70a199b74714a9b82083820ac674.md │ └── 测试_NULL b906a905c34e4fefbea971e923e31d76.md ├── main.py ├── notion_backup.py ├── notion_token.py ├── test ├── __init__.py └── notion_client_test.py └── utils ├── __init__.py ├── config.py └── utils.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | python: circleci/python@1.2 5 | 6 | workflows: 7 | backup-notion: 8 | jobs: 9 | - export-workspace: 10 | filters: 11 | branches: 12 | only: feature/circleci 13 | - publish-github-release: 14 | requires: 15 | - export-workspace 16 | 17 | backup-notion-nightly: 18 | triggers: 19 | - schedule: 20 | cron: "0 0 * * *" # every day UTC±00:00 21 | filters: 22 | branches: 23 | only: 24 | - master 25 | jobs: 26 | - export-workspace 27 | - publish-github-release: 28 | requires: 29 | - export-workspace 30 | 31 | jobs: 32 | export-workspace: 33 | parameters: 34 | branch_build: 35 | type: string 36 | default: master 37 | working_directory: ~/project-export 38 | docker: 39 | - image: cimg/python:3.8 40 | steps: 41 | - run: 42 | name: Check Env 43 | command: | 44 | if [ ! ${GH_TOKEN} ]; then 45 | echo "'GH_TOKEN' not presented!" 46 | exit 1 47 | fi 48 | if [ ! ${NOTION_TOKEN_V2} ]; then 49 | echo "'NOTION_TOKEN_V2' not presented!" 50 | exit 1 51 | fi 52 | - attach_workspace: 53 | at: ~/project-export 54 | - checkout 55 | - run: 56 | name: Python pip install 57 | command: | 58 | pip install requests 59 | pip install python-slugify 60 | - run: 61 | name: Run action 62 | command: | 63 | PYTHONPATH=./ python main.py \ 64 | --token_v2 ${NOTION_TOKEN_V2} \ 65 | --action "all" \ 66 | --output "build/exported" 67 | - run: 68 | name: Dump outputs 69 | command: | 70 | pwd && ls -l && ls -l ~/project-export/build/exported 71 | - run: 72 | name: Collect zip files 73 | command: | 74 | echo "Collect exported zip files:" 75 | if [ -d '~/project-export/build/archives' ]; then 76 | rm -rf ~/project-export/build/archives 77 | fi 78 | mkdir ~/project-export/build/archives 79 | find ~/project-export/build/exported -name "*.zip" -type f | xargs -I '{}' echo '{}' 80 | find ~/project-export/build/exported -name "*.zip" -type f | xargs -I '{}' mv '{}' ~/project-export/build/archives 81 | - persist_to_workspace: 82 | root: ~/project-export 83 | paths: 84 | - ./build/archives 85 | - run: 86 | name: Push unzip files to dist 87 | command: | 88 | # skip push for test 89 | exit 0 90 | 91 | git clone -b feature/backups "https://${GH_TOKEN}@github.com/kaedea/notion-up.git" deploy 92 | cd deploy 93 | git config user.name "Kaede" 94 | git config user.email "kidhaibara@gmail.com" 95 | 96 | # Copy files 97 | if [ -d 'dist' ]; then 98 | rm -rf dist 99 | fi 100 | mkdir dist 101 | echo "Copy unzipfiles:" 102 | cp -r ~/project-export/build/exported/. dist/ 103 | 104 | # Git push 105 | git add * 106 | git status 107 | git commit -a -m "Circleci-bot notion backup - ${CIRCLE_BUILD_NUM}" 108 | git push --force --quiet "https://${GH_TOKEN}@github.com/kaedea/notion-up.git" 109 | 110 | publish-github-release: 111 | parameters: 112 | branch_build: 113 | type: string 114 | default: master 115 | working_directory: ~/project-github-release 116 | docker: 117 | - image: cibuilds/github:0.13 118 | steps: 119 | - attach_workspace: 120 | at: ~/project-export 121 | - run: 122 | name: "Publish Release on GitHub" 123 | command: | 124 | # skip github-release for test 125 | # exit 0 126 | 127 | pwd && ls -l && ls -l ~/project-export/build/archives 128 | VERSION=$(date '+%Y-%m-%d') 129 | ghr -t ${GH_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} ~/project-export/build/archives 130 | 131 | -------------------------------------------------------------------------------- /.config_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "debuggable": false, 3 | "action": "all", 4 | "token_v2": "xxx" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | venv 3 | .idea 4 | *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notion-up 2 | [中文说明](https://www.kaedea.com/2021/10/01/devops/notion-backup/) 3 | 4 | ![](https://www.kaedea.com/assets/8f134329_a1a6_49b2_97a4_c07ea4c3e733_untitled.png) 5 | 6 | NotionUp (Notion Backup) is a python repo helping you to backup notion data automatically. 7 | 8 | ## **Getting Started** 9 | 10 | ### **Prepare** 11 | 12 | To get started with NotionUp, you should: 13 | 14 | 1. Prepare your Notion's username(email) and password, or just find your `notion_token_v2`. 15 | 2. Run `notion-up/main.py` with your configs. 16 | 17 | Check [here](https://github.com/kaedea/notion-down/blob/master/dist/parse_readme/notiondown_gettokenv2.md) to find out your `notion_token_v2` if need. 18 | 19 | ### **Run CLI** 20 | 21 | Basically just run `notion-down/main.py` : 22 | 23 | ```bash 24 | # Run with cli cmd 25 | PYTHONPATH=./ python main.py 26 | --token_v2 27 | --username # Only when token_v2 is not presented 28 | --password # Only when token_v2 is not presented 29 | 30 | # or 31 | PYTHONPATH=./ python main.py \ 32 | --config_file '.config_file.json' 33 | 34 | # Your can configure notion-down args by cli-args, config_file or SysEnv parameters 35 | # Priority: cli args > config_file > SysEnv parameters > NotionDown default 36 | ``` 37 | 38 | ### Archive to GitHub Release 39 | 40 | Check the following workflows and jobs in `.circleci/config.yml` to get how it works. 41 | 42 | ```yaml 43 | workflows: 44 | backup-notion: 45 | jobs: 46 | - export-workspace 47 | - publish-github-release: 48 | requires: 49 | - export-workspace 50 | ``` 51 | 52 | As examples, check the output at [Release](https://github.com/kaedea/notion-up/releases) and [notion-exported](https://github.com/kaedea/notion-up/tree/master/dist). 53 | 54 | ### Backup nightly 55 | 56 | Check the following crontab workflows. 57 | 58 | ```yaml 59 | workflows: 60 | backup-notion-nightly: 61 | triggers: 62 | - schedule: 63 | cron: "0 * * * *" # every hour 64 | filters: 65 | branches: 66 | only: 67 | - master 68 | jobs: 69 | - export-workspace 70 | - publish-github-release: 71 | requires: 72 | - export-workspace 73 | ``` 74 | 75 | ## **Showcase** 76 | 77 | Work with CircleCI, see `.circleci/config.yml`. 78 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733.md: -------------------------------------------------------------------------------- 1 | # NotionDown Posts Template 2 | 3 | This is root post for NotionDown Posts Template. 4 | 5 | [Any notion page here](NotionDown%20Posts%20Template%20f77f3322915a4ab48caa0f2e76e9d733/Any%20notion%20page%20here%20be73b9e8e0924786a91369c4c7c79bbe.md) 6 | 7 | [Page Elements](NotionDown%20Posts%20Template%20f77f3322915a4ab48caa0f2e76e9d733/Page%20Elements%20fdc4e78fe4c14e88993cfae1b2e6b016.md) 8 | 9 | [Page Template](NotionDown%20Posts%20Template%20f77f3322915a4ab48caa0f2e76e9d733/Page%20Template%204c61829a8bf848238b155d8c46321bc9.md) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733/Any notion page here be73b9e8e0924786a91369c4c7c79bbe.md: -------------------------------------------------------------------------------- 1 | # Any notion page here 2 | 3 | Hello, NotionDown! -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733/Page Elements fdc4e78fe4c14e88993cfae1b2e6b016.md: -------------------------------------------------------------------------------- 1 | # Page Elements 2 | 3 | ## NotionDown Properties Block 4 | 5 | ``` 6 | [notion-down-properties] 7 | Title = 8 | Date = 2018-04-01 9 | Published = false 10 | Category = 11 | Tag = Tag1, Tag2 12 | FileLocate = 13 | FileName = 14 | 15 | hexo.comments = true 16 | ``` 17 | 18 | ## More Separator 19 | 20 | 21 | 22 | ## NotionDown Draft Block 23 | 24 | 29 | 30 | ## NotionDown Channel Block 31 | 32 | 39 | 40 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Posts Template f77f3322915a4ab48caa0f2e76e9d733/Page Template 4c61829a8bf848238b155d8c46321bc9.md: -------------------------------------------------------------------------------- 1 | # Page Template 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = 6 | Date = 2018-04-01 7 | Published = false 8 | Category = 9 | Tag = Tag1, Tag2 10 | FileLocate = 11 | FileName = 12 | 13 | hexo.comments = true 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34.md: -------------------------------------------------------------------------------- 1 | # NotionDown Sample 2 | 3 | ## Hello, notion! 4 | 5 | [MarkDown Test Page](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/MarkDown%20Test%20Page%209a873436a8b54f6a9b8ec1be725548a4.md) 6 | 7 | [MarkDown Test Page - SPA](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/MarkDown%20Test%20Page%20-%20SPA%20619ae183133840ad9ca2e51b2f00f0bd.md) 8 | 9 | [MarkDown FileLocate Test](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/MarkDown%20FileLocate%20Test%2077083dacfa0b4b59a6a011832638392b.md) 10 | 11 | [NotionDown README](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20README%20d3463f3d398743879d663caf87efa029.md) 12 | 13 | [NotionDown GetTokenV2](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09.md) 14 | 15 | [NotionDown Custom Config](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Custom%20Config%203c21bc204f0b48c794ff86c6f38fe448.md) 16 | 17 | [NotionDown Table](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Table%20ff82c0e4b9504eb3b0b82549cfb18818.md) 18 | 19 | [NotionDown Obfuscated Blocks](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Obfuscated%20Blocks%2025959a72e55041d6aed69f90226fa45c.md) 20 | 21 | [NotionDown Nested List](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Nested%20List%20ee35f5e58d834865bc38f4e01531e98f.md) 22 | 23 | [NotionDown CN-EN Concat Format](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20CN-EN%20Concat%20Format%204be67947e23843ad81b007bd2ee41c17.md) 24 | 25 | [NotionDown ShortCode](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20ShortCode%20daa1568f28a64afebdc57c43b1401926.md) 26 | 27 | [NotionDown Properties](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Properties%207c43bf2594954ec9816eede682d8e774.md) 28 | 29 | [NotionDown Pullquote Blocks](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Pullquote%20Blocks%205e84918760af4ff78fd8ade884955189.md) 30 | 31 | [NotionDown Image Source](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085.md) 32 | 33 | [NotionDown Spelling Inspect](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/NotionDown%20Spelling%20Inspect%20a6721bfa57da451d8a328fa1f3418969.md) 34 | 35 | [Hexo Integration Post](NotionDown%20Sample%20440de7dca89840b6b3bab13d2aa92a34/Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705.md) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705.md: -------------------------------------------------------------------------------- 1 | # Hexo Integration Post 2 | 3 | [Hexo page - Link Post](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20Link%20Post%209371556035d3412f85ea0f88d8f19b04.md) 4 | 5 | [Hexo page - Japanese Test](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20Japanese%20Test%20975cac84c85249698a1745868ec50616.md) 6 | 7 | [Hexo page - Chinese Test](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20Chinese%20Test%202ac62b5b859246bd9b4d4ff50d223a77.md) 8 | 9 | [Hexo page - Elements Showcase](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20Elements%20Showcase%201cdcf1ab8c0d44fc83a2728f932ca1ba.md) 10 | 11 | [Hexo page - draft post](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20draft%20post%20ef436e83cd1f4d60b636950a177f5f7b.md) 12 | 13 | [Hexo page - NotionDown](Hexo%20Integration%20Post%2001f2281aa8604215a3168ba063d72705/Hexo%20page%20-%20NotionDown%206301820e74974612966ef5638a5b4e8a.md) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - Chinese Test 2ac62b5b859246bd9b4d4ff50d223a77.md: -------------------------------------------------------------------------------- 1 | # Hexo page - Chinese Test 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = 中文測試ト 6 | Date = 2014-01-02 23:30:04 7 | Published = true 8 | Category = Hexo 9 | Tag = Chinsese, Showcase 10 | FileLocate = 11 | FileName = hexo-chinsese-test 12 | 13 | hexo.thumbnailImagePosition = left 14 | hexo.thumbnailImage: http://d1u9biwaxjngwg.cloudfront.net/chinese-test-post/vintage-140.jpg 15 | ``` 16 | 17 | This is a Chinese test post to show you how Chinese is displayed. 18 | 19 | 20 | 21 | 善我王上魚、產生資西員合兒臉趣論。畫衣生這著爸毛親可時,安程幾?合學作。觀經而作建。都非子作這!法如言子你關!手師也。 22 | 23 | 以也座論頭室業放。要車時地變此親不老高小是統習直麼調未,行年香一? 24 | 25 | 就竟在,是我童示讓利分和異種百路關母信過明驗有個歷洋中前合著區亮風值新底車有正結,進快保的行戰從:弟除文辦條國備當來際年每小腳識世可的的外的廣下歌洲保輪市果底天影;全氣具些回童但倒影發狀在示,數上學大法很,如要我……月品大供這起服滿老?應學傳者國:山式排只不之然清同關;細車是!停屋常間又,資畫領生,相們制在?公別的人寫教資夠。資再我我!只臉夫藝量不路政吃息緊回力之;兒足灣電空時局我怎初安。意今一子區首者微陸現際安除發連由子由而走學體區園我車當會,經時取頭,嚴了新科同?很夫營動通打,出和導一樂,查旅他。坐是收外子發物北看蘭戰坐車身做可來。道就學務。 26 | 27 | 國新故。 28 | 29 | > 工步他始能詩的,裝進分星海演意學值例道……於財型目古香亮自和這乎?化經溫詩。只賽嚴大一主價世哥受的沒有中年即病行金拉麼河。主小路了種就小為廣不? 30 | 31 | - *From [亂數假文產生器 - Chinese Lorem Ipsum](*http://www.richyli.com/tool/loremipsum/*)** -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - Elements Showcase 1cdcf1ab8c0d44fc83a2728f932ca1ba.md: -------------------------------------------------------------------------------- 1 | # Hexo page - Elements Showcase 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = Elements showcase 6 | Date = 2015-05-28 20:30:05 7 | Published = true 8 | Category = Hexo 9 | Tag = Html Elements, Showcase 10 | FileLocate = 11 | FileName = hexo-elements-showcase 12 | 13 | hexo.thumbnailImagePosition = left 14 | hexo.thumbnailImage: http://d1u9biwaxjngwg.cloudfront.net/chinese-test-post/vintage-140.jpg 15 | ``` 16 | 17 | Check out how Tranquilpeak theme display well HTML elements (title, paragraph, blockquote, table and more..). It's simple and elegant. 18 | 19 | 20 | 21 | # **Heading 1** 22 | 23 | ## **Heading 2** 24 | 25 | ### **Heading 3** 26 | 27 | **#### Heading 4** 28 | 29 | **##### Heading 5** 30 | 31 | **###### Heading 6** 32 | 33 | ## **Paragraph** 34 | 35 | Lorem ipsum dolor sit amet, [test link]() consectetur adipiscing elit. ****Strong text**** pellentesque ligula commodo viverra vehicula. **Italic text** at ullamcorper enim. Morbi a euismod nibh. Underline text non elit nisl. ~~Deleted text~~ tristique, sem id condimentum tempus, metus lectus venenatis mauris, sit amet semper lorem felis a eros. Fusce egestas nibh at sagittis auctor. Sed ultricies ac arcu quis molestie. Donec dapibus nunc in nibh egestas, vitae volutpat sem iaculis. Curabitur sem tellus, elementum nec quam id, fermentum laoreet mi. Ut mollis ullamcorper turpis, vitae facilisis velit ultricies sit amet. Etiam laoreet dui odio, id tempus justo tincidunt id. Phasellus scelerisque nunc sed nunc ultricies accumsan. 36 | 37 | Interdum et malesuada fames ac ante ipsum primis in faucibus. `Sed erat diam`, blandit eget felis aliquam, rhoncus varius urna. Donec tellus sapien, sodales eget ante vitae, feugiat ullamcorper urna. Praesent auctor dui vitae dapibus eleifend. Proin viverra mollis neque, ut ullamcorper elit posuere eget. 38 | 39 | ## **List Types** 40 | 41 | ### **Definition List (dl)** 42 | 43 |
Definition List Title
This is a definition list division.
44 | 45 | ### **Ordered List (ol)** 46 | 47 | 1. List Item 1 48 | 2. List Item 2 49 | 3. List Item 3 50 | 51 | ### **Unordered List (ul)** 52 | 53 | - List Item 1 54 | - List Item 2 55 | - List Item 3 56 | 57 | ## **Table** 58 | 59 | | Header 1 | Header 2 | Header 3 | 60 | 61 | | :---: | :---: | :---: | 62 | 63 | | Division 1 | Division 2 | Division 3 | 64 | 65 | | Division 1 | Division 2 | Division 3 | 66 | 67 | | Division 1 | Division 2 | Division 3 | 68 | 69 | | Division 1 | Division 2 | Division 3 | 70 | 71 | ## **Misc Stuff - abbr, acronym, sub, sup, etc.** 72 | 73 | Lorem superscript dolor subscript amet, consectetuer adipiscing ctrl + c. Nullam dignissim convallis est. Quisque aliquam. cite. Nunc iaculis suscipit dui. 74 | 75 | Nam 76 | 77 | sit amet sem. Aliquam libero nisi, imperdiet at, tincidunt nec, gravida vehicula, nisl. Praesent mattis, massa quis luctus fermentum, turpis mi volutpat justo, eu volutpat enim diam eget metus. Maecenas ornare tortor. Donec sed tellus eget sapien fringilla nonummy. NBA Mauris a ante. Suspendisse quam sem, consequat at, commodo vitae, feugiat in, nunc. Morbi imperdiet augue quis tellus. AVE -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - Japanese Test 975cac84c85249698a1745868ec50616.md: -------------------------------------------------------------------------------- 1 | # Hexo page - Japanese Test 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = 本語テスト 6 | Date = 2013-01-02 23:30:04 7 | Published = true 8 | Category = Hexo 9 | Tag = Japanese, Showcase 10 | FileLocate = 11 | FileName = hexo-japanese-test 12 | 13 | hexo.thumbnailImagePosition = left 14 | hexo.thumbnailImage = http://d1u9biwaxjngwg.cloudfront.net/japanese-test-post/peak-140.jpg 15 | ``` 16 | 17 | This is a Japanese test post to show you how Japanese is displayed. 18 | 19 | 20 | 21 | 私は昨日ついにその助力家というのの上よりするたなけれ。 最も今をお話団はちょうどこの前後なかろでくらいに困りがいるたをは帰着考えたなかって、そうにもするでうたらない。 がたを知っないはずも同時に九月をいよいよたありた。 22 | 23 | もっと槙さんにぼんやり金少し説明にえた自分大した人私か影響にというお関係たうませないが、この次第も私か兄具合に使うて、槙さんののに当人のあなたにさぞご意味と行くて私個人が小尊敬を聴いように同時に同反抗に集っだうて、いよいよまず相当へあっうからいだ事をしでなけれ。 24 | 25 | > それでそれでもご時日をしはずはたったいやと突き抜けるますて、その元がは行ったてという獄を尽すていけですた。 26 | > 27 | 28 | この中道具の日その学校はあなたごろがすまなりかとネルソンさんの考えるですん、辺の事実ないというご盲従ありたですと、爺さんのためが薬缶が結果までの箸の当時してならて、多少の十月にためからそういう上からとにかくしましないと触れべきものたで、ないうですと多少お人達したのでたた。 29 | 30 | *From [すぐ使えるダミーテキスト - 日本語 Lorem ipsum](http://lipsum.sugutsukaeru.jp/index.cgi)* -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - Link Post 9371556035d3412f85ea0f88d8f19b04.md: -------------------------------------------------------------------------------- 1 | # Hexo page - Link Post 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = Hello, NotionDown! 6 | Date = 2012-01-02 23:30:04 7 | Published = true 8 | Category = Hexo 9 | Tag = Link, Showcase 10 | FileLocate = 11 | FileName = hexo-link-post 12 | 13 | hexo.link = https://github.com/kaedea/notion-down 14 | hexo.thumbnailImagePosition = left 15 | hexo.thumbnailImage = http://d1u9biwaxjngwg.cloudfront.net/hexo-website/peak-140.jpg 16 | ``` 17 | 18 | This is a link post. Clicking on the link should open [Hexo](http://www.hexo.io/) in a new tab or window. 19 | 20 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - NotionDown 6301820e74974612966ef5638a5b4e8a.md: -------------------------------------------------------------------------------- 1 | # Hexo page - NotionDown 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = 基于 Notion 的笔记写作和博客分享自动化方案 6 | Date = 2021-05-20 7 | Published = true 8 | Category = DevOps 9 | Tag = Notion, 知识管理, NotionDown 10 | FileLocate = devops 11 | FileName = notion-to-markdown-file-automating-solution 12 | ``` 13 | 14 | 24 | 25 | 35 | 36 | 个人认为,笔记(Note)、写作(Writing)和分享(Share)是 `个人知识管理` 重要的组成部分。笔记是知识元素,写作是知识汇总,分享是知识和升华。固然每个人具体实践的方式会尽不相同,不过大家应该都能或多或少感到其中存在一些割裂感: 37 | 38 | 其一,笔记存在多端同步编辑的刚需,不过随着云笔记解决方案越来越成熟后,这问题现在已经有许多解决方案。其二,笔记草稿和写作正文之间的同步存在许多机械的地方,同一篇文章经常需要在草稿和正文之间来回修订,而大部分情况下这两者的同步是通过复制粘贴和人工比对来完成的,这个过场是写作体验主要割裂感之一。其三,写作正文完成之后的文章分享(对外部署)也是一个麻烦的流程,尽管现在许多静态博客可以通过自动化技术完成部署,不过文章正文和部署用的 MarkDown 文件之间的管理也是个头疼的事情:如果正文和 MD 文件分开处理,两者之间又只能手动同步;如果直接用 MD 文件来写正文,现在的云笔记方案对 MD 文件的管理体验大都很差;如果干脆使用 git 来同步 MD 文件,那基于 git 的笔记同步方案体验也不会好到哪。 39 | 40 | 自从改用静态博客代替 WordPress 来发表自己的文章和日志后,我一直沿用”云笔记写草稿,MD 文件保存正文,手动在草稿和正文之间同步“这样的原始的写作方案,以上说的几种割裂感我自然也是深有体会。 41 | 42 | 几番苦寻更好的云笔记体验方案未果,直到 ta 的出现:`Notion`。 43 | 44 | 45 | 46 | 49 | 50 | ## Notion 51 | 52 | [Notion](http://notion.so) 是一款时下比较流行的云笔记服务,虽然上线比较晚,不过 ta 却是众多云笔记里面”最靓的仔“。基于巧妙和独特的文本元素关系设计,Notion 可以将你的 Notes、Project Tasks / Plans、Doc / Wikis 等统统整合起来(还提供许多优秀的 Template 以满足不同的文档需要),在 ta 身上我看到了知识管理和项目管理的“大一统”的希望。 53 | 54 | 简单来说,Notion 抛弃了传统以段落(paragraph)为原子单位的做法,而是将所有的一切都当做 Block(是 Everything is Block)。一段文本是一个 TextBlock,一张图片也是一个 ImageBlock,Block 之间可以随意移动和嵌套。同时 Notion Block 被巧妙地设计成组合模式:一个 Block 可以是单一的 Block 也可以是 BlockGroup。这使得 Notion 可以满足几乎所有的文本关系,一片文章本身就是一个 PageBlock,PageBlock 即可以包含各种类型 Block 也可以包含 BlockGroup(用来存放 Table、Columns 等文本组),甚至文章本身就是一个 BlockGroup(类似于 Folder,用于存放一组子文章)。此外,Notion 的强大之处还在于 ta 提供了 CollectionBlock(可以认为是一个简化的 Excel),配合相关的 Notion APIs 我们甚至可以把其当做一个数据库来使用。 55 | 56 | 鉴于 Notion 优秀的多端同步服务和灵活的文本存储功能,我从开始接触到 ta 的时候就产生了“基于 Notion 优化一下自己的知识管理方案”的想法。不过眼下有两个问题还需进一步观察:其一, Notion 一开始是收费的(而且不便宜),我担心收益出现边际效应,成本不可控;其二,Notion 并没有 Official APIs,这会影响基于 Notion 的二次开发的稳定性,而稳定性又是自动化实践里非常重要的考量。 57 | 58 | 随着 Notion 开放免费的个人账号(Personal Plan)和官方 APIs 计划的展开,这些顾虑都再也不是问题。 59 | 60 | ## Solution & Workflows 61 | 62 | 如何优化自己现有的知识管理方案呢,我的基本设想是这样的: 63 | 64 | 1. 基于 Notion 写作,所有的文本材料和源数据都统一放在 Notion 上面同步,避免“冗余写作”。 65 | 2. 使用自动化手段(DevOps)从 Notion 中导出指定笔记的 MD 文件,解决笔记和写作正文之间的割裂感:源文件依然可以保留各种草稿、标注和评论等内容,导出的 MD 文件则根据配置只生成指定的正文内容。 66 | 3. 同样使用自动化手段导出需要分享的 Notion 文章,自动部署到 Hexo 等静态博客。 67 | 68 | 如果有需要,作为兜底策略可以定期导出 Notion 全部笔记数据并做好保存和版本控制,从而弥补 Notion 付费才能使用的历史记录功能和为需要从 Notion 迁移数据这种状况做准备(尝试过云笔记数据迁移的朋友应该知道这是什么考量)。 69 | 70 | 工作流程示意图如下: 71 | 72 | ![../NotionDown%20README%20d3463f3d398743879d663caf87efa029/NotionDown.png](../NotionDown%20README%20d3463f3d398743879d663caf87efa029/NotionDown.png) 73 | 74 | NotionDown 工作流程示意图 75 | 76 | ## NotionDown Project 77 | 78 | 基于 CAP 编程原则,能复制的代码绝不自己写,一开始我抱着侥幸的心态去 GitHub 上面搜索,还真让我找到了类似的工程 [notoma](https://github.com/nategadzhi/notoma)。不过项目还处于 WIP 状态,等了快一年作者还没有什么动作,Demo 也是处于无法运行的状态,所以我索性自己动手好了。 79 | 80 | 自己写一个 3rd-party 的 Notion APIs 不太现实,好在同样的 GitHub 上面已经有先驱做了类似的项目 [notion-py](https://github.com/jamalex/notion-py),相比之下这个项目的完成度已经非常高了(目前唯一的遗憾就是尚未支持 Notion private 笔记的访问,且项目开发文档有限)。 81 | 82 | 基于 notion-py 我写了一个用来支撑自己知识管理的 Notion 笔记导出项目 [notion-down](https://github.com/kaedea/notion-down),主要用来自动从云笔记导出 MD 文件和部署博客(配合 circleci + 静态博客)。 83 | 84 | NotionDown 的主要功能如下: 85 | 86 | 1. 统一在 Notion 平台上编辑笔记(集大成)。 87 | 2. 基于 Notion 笔记 + 相应的编译配置,解析所需的 MD 文件(必须支持图片配置)。 88 | 3. 根据配置将需要 Publish 的文章自动部署指定的静态博客(Hexo)。 89 | 4. 相关配套的集成功能:图床配置,自定义短代码(如生成的文章内容按渠道配置动态调整),中英混排优化(pangu),拼写检查(双拼用户刚需)。 90 | 91 | 如此一来,以后岂不是可以专心写作了?🤣~~(鬼咧,正经 Bloger 谁写文章啊,不都是在折腾博客主题吗。)~~ 92 | 93 | ## Substitute 94 | 95 | 如果只是需要自动把 Notion 上面的笔记部署到静态博客,我这发现一个更简单的解决方案:[Notion + GatsbyJs + Netlify 极致的博客体验](https://chenhuichao.com/c32f80ee1ca84d45aaf63ee170e3c267)。 96 | 97 | 其基本思路是通过 Netlify 作为 Trigger 触发 GatsbyJs 插件服务读取 Notion 笔记数据,并存放到 Gatsby 平台,最终通过 Gatsby 提供的博客服务展示博客内容。有兴趣可以了解一下 [Gatsby](https://www.gatsbyjs.com/),这套方案可以节省 circleci 和静态博客 generating 等不少中间流程。 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/Hexo Integration Post 01f2281aa8604215a3168ba063d72705/Hexo page - draft post ef436e83cd1f4d60b636950a177f5f7b.md: -------------------------------------------------------------------------------- 1 | # Hexo page - draft post 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = Hexo website 6 | Date = 2016-01-02 23:30:04 7 | Published = false 8 | Category = tranquilpeak, features 9 | FileLocate = 10 | FileName = hexo-draft-post 11 | 12 | hexo.link = http://www.hexo.io/ 13 | ``` 14 | 15 | This is a link post. Clicking on the link should open [Hexo](http://www.hexo.io/) in a new tab or window. 16 | 17 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b.md: -------------------------------------------------------------------------------- 1 | # MarkDown FileLocate Test 2 | 3 | ``` 4 | [notion-down-properties] 5 | FileLocate = 6 | ``` 7 | 8 | This is a simple page for property `FileLocate` test with non-presented case. 9 | 10 | [MarkDown FileLocate Test R2](MarkDown%20FileLocate%20Test%2077083dacfa0b4b59a6a011832638392b/MarkDown%20FileLocate%20Test%20R2%2088187dd965a042cb97e6eaee6bc34d0b.md) 11 | 12 | [MarkDown FileLocate Test R3](MarkDown%20FileLocate%20Test%2077083dacfa0b4b59a6a011832638392b/MarkDown%20FileLocate%20Test%20R3%20a2d3447bac4b4f379cb8b0cd744b5956.md) 13 | 14 | [MarkDown FileName Test](MarkDown%20FileLocate%20Test%2077083dacfa0b4b59a6a011832638392b/MarkDown%20FileName%20Test%205862241766b548f9a6d47f446223703f.md) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b/MarkDown FileLocate Test R2 88187dd965a042cb97e6eaee6bc34d0b.md: -------------------------------------------------------------------------------- 1 | # MarkDown FileLocate Test R2 2 | 3 | ``` 4 | [notion-down-properties] 5 | FileLocate = file-locate 6 | ``` 7 | 8 | This is a simple page for property `FileLocate` test with presented case. -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b/MarkDown FileLocate Test R3 a2d3447bac4b4f379cb8b0cd744b5956.md: -------------------------------------------------------------------------------- 1 | # MarkDown FileLocate Test R3 2 | 3 | ``` 4 | [notion-down-properties] 5 | FileLocate = file-locate/r3 6 | ``` 7 | 8 | This is a simple page for property `FileLocate` test with presented case. -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown FileLocate Test 77083dacfa0b4b59a6a011832638392b/MarkDown FileName Test 5862241766b548f9a6d47f446223703f.md: -------------------------------------------------------------------------------- 1 | # MarkDown FileName Test 2 | 3 | ``` 4 | [notion-down-properties] 5 | FileName = NotionDown FileName Test 6 | ``` 7 | 8 | This is a simple page for property `FileName` test. -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page - SPA 619ae183133840ad9ca2e51b2f00f0bd.md: -------------------------------------------------------------------------------- 1 | # MarkDown Test Page - SPA 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = 增量静态检查(SPA)在代码合入检查里的应用 6 | Date = 2018-04-01 7 | Published = true 8 | Category = 9 | Tag = 10 | FileLocate = devops 11 | FileName = incremental-spa 12 | ``` 13 | 14 | 静态程序分析,是指在不运行程序的情况下分析检查代码里存在的问题。这项技术在代码质量、漏洞扫描等领域有广泛的使用。常见分析工具包括 CheckStyle、Lint、FindBugs 等,也有商用的 Coverity。本文主要讲述为我们在 Android 项目 Merge Request 合入检查里对静态程序分析技术的应用,核心内容是增量代码的静态分析方案,至于各种检查工具的对比筛选,请参考文末提供的 References。 15 | 16 | ## **名词解释** 17 | 18 | 1. 静态程序分析:SPA (Static Program Analysis),也称静态代码检查、静态扫描、静态检查等(下文统称 “静态检查”) 19 | 2. 代码合入检查:泛指代码提交进主干分支前的一些列检查流程,比较有代表性的是 GitHub PR (Pull Request) 或者 GitLab MR (Merge Request) 合并前进行自动化检查流水线、或者 Code Review 工作(下文统称 “静态检查”,指 MR 合并前的代码检查) 20 | 21 | ## **问题背景** 22 | 23 | 微信相关 Android 项目的 DevOps 实践中,我们在合入检查方向已经先后完成了 “需求合法性检查”、“代码冲突检查”、“编译检查”、“编译后 WeTest 自动化 UI 测试” 等检查项目,代码合入检查流程已经比较完善。接下来,我们想尝试在检查流程里加入静态检查环节,看看能不能在 “统一代码风格、提高代码质量” 方面实现一些突破。 24 | 25 | 传统上,代码风格检查普遍比较依赖于人工的 Code Review,而 DevOps 实践给我们的经验是,代码格式、一般向代码错误等问题交给工具自动化处理比较合适,人工 Review 的主要目的应该是项目方案评审,以及优秀代码学习,不然的话 Code Review 很可能会变成政治任务,流于形式。因此,我们希望借助静态检查工具,先过滤大部分的一般代码问题,再交由人工进行代码设计方面的 Review 或者学习(注意,本文侧重于静态检查工具的使用,至于具体 Code Review 标准、流程请参考其他文献)。 26 | 27 | 静态检查对 Code Review 起到一个支撑作用: 28 | 29 | > 先由静态检查工具过滤常见的错误,工具无法判断的问题可以给出先 warning log,人工 Review 再根据静态检查的 log,重点排查可疑代码 30 | > 31 | 32 | 不过实际应用上,静态检查工具的接入还是存在许多麻烦的问题,特别是对于一些比较成熟、历史包袱严重的项目。一方面,我们相信有不少人已经尝试过使用一些静态检查工具,这些工具在一些小项目上,经常一下子就能跑出一大堆问题,而对于比较庞大的项目,扫描出太多问题基本相当于扫描不出问题,所以我们不得不想办法让检查工具专注于我们关心的问题。另一方面,一般的静态检查工具的分析过程都比较耗时,少则几分钟,一些需要依赖编译产物的工具耗时可能达到十几分钟(类似 Coverity 这种商业功能需要依赖多维度的数据作为数据流分析的依据,耗时更是可能达到小时的级别),这种量级的时间要求对合入检查流程来说是不可接受的,特别是封版前这种时间十分紧迫的版本阶段,更不用说我们的最终目标是希望在用户本地开发代码阶段就把检查流程添加进来,因此在这个流程上我们需要对静态检查工具的性能提一个非常高的要求。 33 | 34 | 总结一下问题,现在摆在我们面的主要有 “两个矛盾”: 35 | 36 | > 静态检查通常会检查出大量的 “陈年老代码” 带来的历史遗留问题,而这些问题大部分没人维护,也不能随意修改;而合入检查则要求只检查出新增代码带来的新问题静态检查耗时普遍比较 “可观”,越是要求检查精准,越是需要更多耗时;而静态检查则要求越快越好 37 | > 38 | 39 | ## **解决思路** 40 | 41 | ### **1. 如何让静态检查工具只检查出新增部分代码带来的问题** 42 | 43 | 介绍我们的方案之前,先说一说两个使用得比较普遍的方案:其一,根据代码提交的时间(例如 git 工具就可以检查每一行代码最近的时间),约定一个起始点时间(比如上一个稳定版本)作为 baseline,静态检查工具检查出来的问题,其对应的代码提交记录如果早于这个 baseline 则自动忽略该问题,这样就能从一大堆问题里面筛选出比较 “新鲜” 的问题。其二,选一个稳定版本作为 baseversion,扫描出这个版本所有的问题并把结果记录下来,以后每次静态扫描都和前面 baseversion 保留的记录做比对,屏蔽存量问题(或者直接编写脚本,根据 baseversion 扫描出来的结果,给全部存量问题自动加上 suppression 注释以便后续静态检查在扫描阶段就屏蔽问题)。 44 | 45 | 公司内部部分静态扫描服务使用的就是以上两个方案之一(比如 CodeCC 使用的是 baseversion 方案),这样做的好是思路清晰接入点也简单,几乎对于所有的静态检查工具都可以使用这些方案,也不用担心静态检查工具版本升级带来的兼容性问题。不过缺点也是显而易见的,首先是依然要重复检查老问题,白白浪费资源,而且有些新问题结果表现可能和老问题一样,导致这些问题会被当成老问题忽视。 46 | 47 | 再来说说我们现在使用的方案:DevOps 实践中,我们需要计算出用户改了哪些代码或文件(也就是用户提交的 “增量代码”),用来检查用户的行为是否合法(比如改了不允许修改的问题,或者动了别人的代码需要 @owner 过来 Review),因此我们首先想到的是可以直接利用这些增量代码,在静态检查结果中匹配出增量代码带来的问题,这样虽然无法一下解决检查效率的问题,但是也能保证匹配出来的问题大概率是和用户改动的代码相关的。再做进一步思考,既然我们已经得到代码的增量数据,我们是不是可以直接对这部分增量的代码做静态检查?这样既不用重复检查那一大堆成年老问题,也可以直接暴露出增量代码带来的问题。答案是肯定的,我们最终采取了这两种方式结合的思路: 48 | 49 | > 直接对增量代码做检查得到一批问题报告,再从中匹配出增量代码带来的问题。 50 | > 51 | 52 | ### **2. 如何提高静态检查的效率** 53 | 54 | 静态检查的效率,一方面跟扫描的文件数量相关,另一方面也受工具自身扫描算法、扫描内容、检测规则的粒度、规则的数量等影响。想要提高效率,一方面我们需要尽量减少输入的文件数,而我们上面提到的增量检查思路刚好把这个方面的问题做到最优解。另一方面,至于扫描算法,自己开发更加高效的检查工具显然不太现实,所以我们把目光放到扫描内容和检测规则,好在现在主流的静态检查工具,其检查规则甚至检查粒度,大都支持用户自定义。简单地说,针对源码类型做扫描的工具比如 CheckStyle 和 Lint,需要经过词法分析、语法分析生成抽象语法树,再遍历抽象语法树跟定义的检测规则去匹配,其工作效率会比较高;而像针对 .class 字节码文件做扫描的工具 FindBugs,需要先编译源码成 .class 文件,再通过 BCEL 分析字节码指令并与探测器规则匹配,效率就会大打折扣(Lint 也支持这种检查方式,这里不做展开)。除了以上谈到的两点外,像 Android 这种 Gradle 项目,如果项目 Module 比较多,Gradle Configure 阶段也会需要比较多的耗时。 55 | 56 | 实际上,第一个问题的解决思路,已经给现在这个问题指明了一个方向:针对增量代码做检查,既减少输入文件,又降低需要执行检查任务的 Module 数。万事俱备,接下来的事就是选用合适的静态检查工具了。 57 | 58 | 对于我们的项目,目标是 “统一代码风格、提高代码质量”。统一风格方面,CheckStyle 当仁不让, 它支持直接对代码源文件进行扫描,并且内置许多成熟的 Style Guides/Conventions 方案(比如 Google/Sun/Oracle),而且自定义规则也非常简单,完全可以自定义自己的代码格式和变量命名规则。剩下的就是如何提高代码质量,我们选用的是 Lint,理由非常简单,Lint 是 Android 官方深度定制工具,和 Android 项目相性最好,功能极其强大,且可定制性和扩展性以及全面性都表现均超乎我们期待。平时编写代码过程中,Android Stuido 智能标注的各种红色(Error)、黄色(Warning)警告,大部分都是 Lint 检查的结果(所以有事没事推荐大家多按 F2 试试,自动跳到下一处 Lint 检查出问题的地方)。除了跟 IDE/Gradle 插件相结合得很好之外,Lint 也像 CheckStyle 一样,支持直接对代码源文件、甚至 XML 资源文件进行检查,不需要计算依赖或者 API References,效率比较理想。而且 Lint 自定义检查规则的功能也非常强大,几乎覆盖对大部分 Java 代码、XML 资源格式的检查。 59 | 60 | 我们的整体方案是: 61 | 62 | > 计算增量代码以增量代码涉及的源文件作为输入(粒度是文件),给项目相关 Module 配置相应的 CheckStyle 和 Lint 任务运行检查任务,根据预置的检查规则进行扫描,得到检查结果从检查结果中筛选出和增量代码相关的问题(粒度是文件修改行数) 63 | > 64 | 65 | ## **具体方案实现** 66 | 67 | ### **1. 通过 git 计算增量代码的信息** 68 | 69 | 在计算代码增量信息方面,我们需要解决以下几个问题: 70 | 71 | > 需要计算本地修改代码的增量信息,在体检代码前检查一下有没有新增问题需要计算提交 MR 时,开发分支与主干分支之间代码的增量信息,用于检查 MR 带来的新增问题当用户 MR 检查出问题后在本地修改代码,我们需要计算出 “开发分支与主干分支之间代码的增量信息” 叠加 “用户新修改问题带来的增量信息” 后的最终结果,以便用户检查自己是否已把问题修复 72 | > 73 | 74 | 我们计算代码增量信息的方案,全靠 `git diff` 命令: 75 | 76 | ![https://kaedea.com/assets/images/spa/img-git-diff.png](https://kaedea.com/assets/images/spa/img-git-diff.png) 77 | 78 | 这里先简单介绍一下我们方案需要用到的这几个参数的作用: 79 | 80 | 1. `name-only`:只显示修改文件的名字,不显示内容,这个参数分别刚好满足上面提到的方案的 “文件粒度” 和“文件修改行数粒度” 81 | 2. `-cached`:只显示被 staged 过的文件(也就是已经被 add 到 git index 里),计算本地修改代码的时候,需要通过这个参数计算 “已 staged” 和“未 staged”两种文件的综合数据 82 | 3. `-diff-filter=ACMR`: 显示哪类文件:Added (A)、Copied (C)、 Modified (M)、Renamed (R),具体配置请参看官方文档 83 | 4. `-ignore-space-at-eol`: 忽略行尾空格或者换行符等修改信息,这里额外说明一下,MR 计算代码修改信息用的也是 git diff 工具而且默认不带这个参数,而 IDEA 的 Annotate 默认是带这个参数的,所以有时候会看到有人提交 MR 显示修改了大量代码(比如批量代码格式化导致修改文件换行符),而当事人本地 IDEA 又看不出自己改了代码 84 | 5. `M100%`:这是一个阈值,用来计算前后两个不同名字的文件,内容相似度达到哪个百分比就认为这是一个被 Renamed (R) 的文件 85 | 86 | 关于参数解释,这里推荐一个工具 [explainshell.com](https://explainshell.com/explain?cmd=git+diff+--name-only+--cached+--diff-filter%3DACMR+--ignore-space-at-eol+-M100%25),不在赘述。现在回答上面提到的几个问题。 87 | 88 | 对第一个问题,通过 `git diff --diff-filter=ACMR --ignore-space-at-eol -M100%` 和 `git diff --cached --diff-filter=ACMR --ignore-space-at-eol -M100%` 两个命令组合起来,可以综合计算出本地变动代码的增量信息。至于第二个问题,可以先通过 `git merge-base HEAD` 计算出当前分支跟主干分支之间的共同节点 merge_base_revision,再通过 `git diff HEAD`,就可以计算出当前开发分支从主干分支拉出来之后的代码变动情况。 89 | 90 | 这个方案里最难也最容易被忽略的是第三个问题,不像第一个问题里只要分别计算 staged 和 unstaged 两种文件再加起来就完事了,这里的本地的修改数据不是简单的叠加问题:用户修改了个文件 X,代码提交后,开发分支和主干之间就有一份确定的文件 X 的修改信息 A,这时候用户在本地继续修改文件 X,那本地修改记录里文件 X 也有另一份修改信息 B,这时候 B 跟 A 的修改信息是完全冲突的,我们要的是用户修改文件后本地文件最终状态跟主干之间的代码差异 C,而 A 跟 B 都是错误的信息,而且很明显 C ≠ A + B。 91 | 92 | 为了解决第三个问题,我们前后设计了两套方案: 93 | 94 | 1. 计算增量信息的时候,先 `git add -A && git commit` 自动提交本地修改代码,然后计算 `git merge-base HEAD`,最后 `git reset --soft HEAD~1` 把自动提交撤销掉。更详细的操作,还可以先计算修改文件哪些是 staged 的,reset 之后需要把 staged 状态恢复,全面还原文件状态。 95 | 2. 依次计算修改信息 A 和修改信息 B,遍历修改信息 B 的每一行文件修改内容,然后一次拓扑更新 A 的内容,直到遍历完全 B,这时候拓扑后的 A 就是目标的信息 C 了。 96 | 97 | 方案 1 的好处就是逻辑清晰,但是提交和撤销动作涉及本地文件修改,容易出现文件修改冲突破坏现场。方案 2 逻辑和算法都比 1 要复杂得多,好在经过几次迭代后我们证明这个路子是可行,现在稳定投产中。 98 | 99 | 作为补充说明,`git diff` 命令输出的格式不太适合直接参与后面的计算工作,需要先转换成程序友好的格式(比如 JSON),这里推荐一款基于 Python 的 git diff 解析工具:[git-diff-parser](https://github.com/nathforge/gitdiffparser)。项目中我们采用的是 Gradle 插件,所以也用 Groovy 实现了类似的解析工作。假设我们修改了 `MainActivity.java` 文件,新增了 `basepacks.txt` 文件,`git diff` 解析前格式是: 100 | 101 | ``` 102 | diff --git a/app/src/main/java/com/example/app/MainActivity.java b/app/src/main/java/com/example/app/MainActivity.java 103 | index d3151dd..80ba56b 100644 104 | --- a/app/src/main/java/com/example/app/MainActivity.java 105 | +++ b/app/src/main/java/com/example/app/MainActivity.java 106 | @@ -7,6 +7,8 @@ import android.widget.Toast; 107 | 108 | import com.jianghongkui.customelint.R; 109 | 110 | +import java.io.IOException; 111 | + 112 | public class MainActivity extends AppCompatActivity { 113 | 114 | @Override 115 | @@ -15,11 +17,26 @@ public class MainActivity extends AppCompatActivity { 116 | setContentView(R.layout.activity_main); 117 | 118 | - Toast.makeText(this, "", 20); 119 | - assert true; 120 | + Toast.makeText(this, "", 20); 121 | + assert true; 122 | + String hello = "hello"; 123 | + System.out.println(hello); 124 | + Integer.parseInt("2"); 125 | + Float.parseFloat("2"); 126 | + System.loadLibrary("hello"); 127 | + try { 128 | + getAssets().open("hello"); 129 | + } catch (IOException e) { 130 | + e.printStackTrace(); 131 | + } 132 | + 133 | + foo("hello2"); 134 | + foo("libandromeda"); 135 | + System.out.println("libflutter"); 136 | + System.out.println("libflutter_v7.so"); 137 | + } 138 | 139 | - System.out.println("hello"); 140 | - Integer.parseInt("2"); 141 | - Float.parseFloat("2"); 142 | + private void foo(String str) { 143 | + System.loadLibrary(str); 144 | } 145 | } 146 | diff --git a/add.txt b/add.txt 147 | new file mode 100644 148 | index 0000000..9b07058 149 | --- /dev/null 150 | +++ b/add.txt 151 | @@ -0,0 +1,2 @@ 152 | +asad 153 | +asdasd 154 | \ No newline at end of file 155 | 156 | ``` 157 | 158 | 经过解析之后变成: 159 | 160 | ``` 161 | [ 162 | { 163 | "file": "app/src/main/java/com/example/app/MainActivity.java", 164 | "changed_lines": [ 165 | 10, 166 | 11, 167 | 20, 168 | 21, 169 | 22, 170 | 23, 171 | 24, 172 | 25, 173 | 26, 174 | 27, 175 | 28, 176 | 29, 177 | 30, 178 | 31, 179 | 32, 180 | 33, 181 | 34, 182 | 35, 183 | 36, 184 | 37, 185 | 39, 186 | 40 187 | ], 188 | "deleted_lines": [ 189 | 18, 190 | 19, 191 | 21, 192 | 22, 193 | 23 194 | ], 195 | "is_new_file": false 196 | }, 197 | { 198 | "file": "add.txt", 199 | "changed_lines": [ 200 | 1, 201 | 2 202 | ], 203 | "deleted_lines": [], 204 | "is_new_file": true 205 | } 206 | ] 207 | 208 | ``` 209 | 210 | 具体代码请参考文末提供的代码仓库。 211 | 212 | ### **2. 基于 AGP 插件的 Lint 增量检查方案** 213 | 214 | 在增量检查的具体实现上,我们采用的是自定义 Gradle 插件,增加了 `:checkIncremental` 任务来执行增量的检查任务。其中 CheckStyle 检查的实现是基于 Gradle Api 提供相关 Checkstyle Task,这个本身就支持增量检查,直接配置输入文件就好。Lint 的增量检查就要复杂得多,主要是基于 Android 官方的 AGP (Android Gradle Plugin) 插件提供的 `com.android.tools.lint:lint-gradle` 库进行实现,以下介绍几个关键的技术点。 215 | 216 | ### **Lint 检查的工作流程** 217 | 218 | Android Lint 的工作过程比较简单,由一个基础的 Lint 过程由 Lint Tool(检测工具),Source Files(项目源文件) 和 lint.xml(配置文件) 三个部分组成:Lint Tool 读取 Source Files,根据 lint.xml 配置的规则(issue)输出结果,如下图: 219 | 220 | ![https://kaedea.com/assets/images/spa/image-lint-workflow.png](https://kaedea.com/assets/images/spa/image-lint-workflow.png) 221 | 222 | Android 项目中,一般我们有三种方式运行 啊走Lint 检查:命令行、IDEA 的 Inspections 检查功能、Gradle Lint 任务,他们都由 [AGP Android Lint](http://tools.android.com/tips/lint) 提供,并由 Android 官方进行维护,虽然检查入口各不相同,但是底层都是同一套 Lint API 实现(提供 Lint 检查实现的 lint-api.jar 和封装好一些常用检查规则的 lint-check.jar,三种工作方式都是基于这两个类库实现)。 223 | 224 | 届于 Lint API 涉及的类库比较复杂,这里不做深入讨论,主要介绍一下几个比较关键的 API: 225 | 226 | 1. `LintDriver`: 三种工作方式最后都通过 LintDriver#analyse() 执行实际上的检查工作。 227 | 2. `IssueRegistry`:管理 Lint 检查规则,配合 lint.xml 使用。Android 有内置的 BuiltinIssueRegistry,用户自定义检查的话,需要重写该类。 228 | 3. `LintRequest`:执行 Lint 检查时的一个请求类,封装好了需要检查的文件内容,我们需要实现的增量检查,也是要从这里下手。 229 | 4. `LintClient`:Lint 客户端,集成 Lint 检查的操作、配置,对应某一种具体的工作方式,比如 LintCliClient 对应命令行方式,LintIdeClient 对应 IDEA 的 Inspections,LintGradleClient 对应 Gradle Lint Task。 230 | 231 | 用伪代码表示的话大概是: 232 | 233 | ``` 234 | def registry = new IssueRegistry() 235 | def client = new LintClient(registry) { 236 | override createLintRequest(file) { 237 | return new LintRequest(file) 238 | } 239 | override run() { 240 | LintDriver.analyze() // super impl 241 | } 242 | } 243 | client.run() 244 | 245 | ``` 246 | 247 | ### **Lint 自定义检查规则** 248 | 249 | 自定义检查规则,就是要自定义各种检查 Detector 类,具体可以参考官方的指导文档 [Writing a Lint Check](http://tools.android.com/tips/lint/writing-a-lint-check) 或者美团的几篇 Lint 实践文章 [美团 Lint](https://tech.meituan.com/tags/lint.html)。老实说,这方面官方给出的 Example 并不是很详细,具体怎么写还是要靠自己去看官方 Lint API 的源码,以及参考别人的开源代码(如果你比较熟悉 Visitor 访问者模式,或者写过 ASM 插件,应该比较容易上手)。 250 | 251 | 这里给出一个我们自己自定义的检查规则作参考: 252 | 253 | ``` 254 | // 检查 Log 的使用规范 255 | public class LogIssue extends Detector implements Detector.UastScanner { 256 | 257 | public static final Issue ISSUE = Issue.create( 258 | "WxLog", 259 | "Log 使用不规范, 请使用 platformtools.Log", 260 | "请使用项目共用的Log函数, 输出console及xlog文件请使用platformtools.Log, 不希望输出xlog文件请使用android.util.Log", 261 | Category.CORRECTNESS, 262 | 9, 263 | Severity.ERROR, 264 | new Implementation(LogIssue.class, Scope.JAVA_FILE_SCOPE)); 265 | 266 | @Override 267 | public void visitMethod(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) { 268 | if (context.getEvaluator().isMemberInClass(method, "java.io.PrintStream")) { 269 | context.report(ISSUE, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT)); 270 | } 271 | } 272 | 273 | @Override 274 | public List getApplicableMethodNames() { 275 | return Arrays.asList("print", "println"); 276 | } 277 | } 278 | 279 | ``` 280 | 281 | ### **Lint 增量检查的实现方案** 282 | 283 | 上面提到,“我们需要实现的增量检查,需要从 LintRequest 下手”,通过重写 LintClient 中的 createLintRequest 方法,传入我们需要增量检查的文件,以下给出关键的代码,具体的实现细节,请参考我们的代码库。 284 | 285 | ``` 286 | class LintToolClient extends LintCliClient { 287 | 288 | @Override 289 | protected LintRequest createLintRequest(List files) { 290 | LintRequest request = super.createLintRequest(files) 291 | for (Project project : request.getProjects()) { 292 | for (File file : files) { 293 | project.addFile(file) // 具体需要检查的文件 294 | } 295 | } 296 | return new LintRequest(this, files) 297 | } 298 | } 299 | 300 | ``` 301 | 302 | 上面提到有三种 Lint 的工作方式,我们采用的是扩展第三种 Lint Gradle 方式(因为这种方式能直接复用现成的 lint.xml 配置文件和 Lint Output 报告格式),最终整体的工作流程是: 303 | 304 | > 通过 git diff 获得需要检查文件作为 Source Files。根据自己的检查需要自定义 LintIssueRegistry(增加了一批自定义的 Detector,考虑的性能需要,移除了那些需要依赖编译产物的内置 Detectors)。自定义继承 LintGradleClient 的 LintIncrementalClient(也就是采用 Lint Gradle 工作方式)。根据 Source Files,为每一个有文件变动的 Module 创建增量检查用的 Gradle Task;运行检查任务的时候,执行 LintIncrementalClient#run() 实现增量检查 305 | > 306 | 307 | 除了我们采用的第三种 Lint Gradle 方式,这里补充说明一下第二种 IDEA 的 Inspections 功能:通过 “IDEA - Analyse - Inspect Code” 可以迅速针对一个指定的文件做 Lint 检查。如下: 308 | 309 | ![https://kaedea.com/assets/images/spa/image-idea-inspect-code.png](https://kaedea.com/assets/images/spa/image-idea-inspect-code.png) 310 | 311 | 经过挖掘,我们发现其实现的关键代码在 [LintIdeClient.java](https://github.com/JetBrains/android/blob/master/android/src/com/android/tools/idea/lint/LintIdeClient.java)。通过 Inspections 方案,我们能直接对指定文件进行 Lint 检查,而不需要依赖于 Gradle 环境,这是 Lint 增量检查的最佳方案。不过考虑到 IDEA 版本之间的兼容性问题,而且我们还需要将检查工作合入到 DevOps 自动化流程里,所以最终还是选择了 Gradle Lint 方案。 312 | 313 | 最后,关于具体的 Lint 实现有一点需要补充说明:Lint API 25 到 26 之间,无论是 API 接口还是具体的实现,变化都非常大,所以各位参考别人的具体实现代码的时候,一定要先分清当前的 API 版本是多少。 314 | 315 | ### **3. 整体工作流程图** 316 | 317 | 没有流程图的方案是没有灵魂的,如下: 318 | 319 | ![https://kaedea.com/assets/images/spa/image-workflows.png](https://kaedea.com/assets/images/spa/image-workflows.png) 320 | 321 | ## **结果沉淀** 322 | 323 | 以代码仓库的 Demo 项目为例,如果执行一遍默认的 `:app:clean :app:lint` 检查任务,耗时 Configure + 检查任务整体耗时大概在 10s 左右,如下: 324 | 325 | ![https://kaedea.com/assets/images/spa/image-demo-1.png](https://kaedea.com/assets/images/spa/image-demo-1.png) 326 | 327 | 接入增量 Lint 方案后,耗时已经能压缩到 1s 左右: 328 | 329 | ![https://kaedea.com/assets/images/spa/image-demo-1.png](https://kaedea.com/assets/images/spa/image-demo-1.png) 330 | 331 | 即使加上增量的 CheckStyle 检查任务,再最终补上一个用来做检查结果报告的 `checkReport` 任务,整体增量检查耗时也能稳定在 1s: 332 | 333 | ![https://kaedea.com/assets/images/spa/image-demo-1.png](https://kaedea.com/assets/images/spa/image-demo-1.png) 334 | 335 | 在实际项目 MR 合入检查流水线里的应用效果如下: 336 | 337 | 在 MR 代码合入检查的静态检查环节上,我们目前一共实现了 CheckStyle、Lint、文件格式(LF/CRLF 换行符问题)、非法文件修改(文件权限)四种检查内容,其中 Lint 增量带来的收益最明显,时间成本从原本的几分钟、十几分钟级别下降到几秒到几十秒的级别(通常只要在封版前涉及大量代码修改的 MR 才需要几十秒的耗时),已经基本满足了我们 DevOps 合入检查的要求(考虑到静态检查环节是我们合入检查几个并行的 Stages 之间耗时最小的一个,可以说相当于没有时间成本)。而且除了时间成本之外,Lint 自定义检查的功能相当于给我们的平台提供了一种定制性比较强的检查工具,比如 Dark Mode 对 XML 的颜色值有使用规范,通过自定义 Detector 可以很轻松得检查每一个新增 XML 文件的 color 属性。 338 | 339 | ## **尾巴** 340 | 341 | 本文主要以介绍静态检查整体的应用方案为主,以及分享方案落地流程里一些问题,主要是一己的经验之谈。如果你希望了解现有静态检查工具的对比和应用,这方面市场上已经有大量的科普和评测文章请自行检索,如果你希望试用各种检查工具,这里推荐一下公司内部的静态检查服务 CodeCC 和 CodeDog,他们都有详细的使用文档。又或者你想研究 Lint API 具体的工作细节,这里推荐一下美团技术团队编写的几篇 Lint 相关技术文章 [美团 Lint](https://tech.meituan.com/tags/lint.html)。 342 | 343 | ## **参考链接** 344 | 345 | 1. [http://tools.android.com/tips/lint](http://tools.android.com/tips/lint) (官方文档) 346 | 2. [https://tech.meituan.com/tags/lint.html](https://tech.meituan.com/tags/lint.html) (美团 Lint 实践) 347 | 3. [https://www.jianshu.com/p/a0f28fbef73f](https://www.jianshu.com/p/a0f28fbef73f) (自定义 Lint 规则) 348 | 4. [https://blog.csdn.net/ouyang_peng/article/details/80374867](https://blog.csdn.net/ouyang_peng/article/details/80374867) (自定义 Lint 规则) 349 | 5. [https://www.jianshu.com/p/4833a79e9396](https://www.jianshu.com/p/4833a79e9396) (增量 Lint 实现) 350 | 6. [https://github.com/lingochamp/okcheck](https://github.com/lingochamp/okcheck) (增量检查,粒度是有代码改动的 Module,一个折中的方案,优点是侵入性小) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4.md: -------------------------------------------------------------------------------- 1 | # MarkDown Test Page 2 | 3 | [Table](MarkDown%20Test%20Page%209a873436a8b54f6a9b8ec1be725548a4/Table%20f802ebd06ae241c989da3c1c66042ea0.csv) 4 | 5 | ``` 6 | [notion-down-properties] 7 | Title = 8 | Date = 2021-05-01 9 | Published = false 10 | Category = 11 | Tag = 12 | FileLocate = 13 | FileName = 14 | ``` 15 | 16 | This is the first paragraph: 17 | 18 | 1. First item 19 | 2. Second item on a numbered list. 20 | 3. Third item 21 | 22 | - Unordered 23 | - List here 24 | - 3rd item 25 | 26 | [This is a link](https://respawn.io) 27 | 28 | > Quoted block 29 | > 30 | 31 | Callout test 32 | 33 | - Toggle Block 1 34 | 35 | Hello, Toggle Block 1 36 | 37 | - Toggle Block 2 38 | 39 | Hello, Toggle Block 2 40 | 41 | 42 | ![MarkDown%20Test%20Page%209a873436a8b54f6a9b8ec1be725548a4/kiminonaha_tenkinoko_2.jpg](MarkDown%20Test%20Page%209a873436a8b54f6a9b8ec1be725548a4/kiminonaha_tenkinoko_2.jpg) 43 | 44 | this is image 45 | 46 | --- 47 | 48 | # Heading 49 | 50 | ## Subheading 51 | 52 | ### Subsubheadling 53 | 54 | #### Heading 55 | 56 | ##### Heading 57 | 58 | ###### Heading 59 | 60 | ```python 61 | def func(self): 62 | pass 63 | ``` 64 | 65 | RichText 66 | 67 | This is a rich text: **B**, *I,* U, ~~D~~,`R` 68 | 69 | --- 70 | 71 | Commnet -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4/Table f802ebd06ae241c989da3c1c66042ea0.csv: -------------------------------------------------------------------------------- 1 | Title,Number,Select,MultiSelect,Date,Persson,Files,CheckBox,Url,Email,Phone 2 | 测试,123456,Select_ITEM_01,"MULTI_SELECT_ITEM_02, Multi_SELECT_ITEM_01","May 1, 2021 12:00 AM",Kaede Akatsuki,https://www.notion.so/a88e9b9088654cfbbdf9c614f5a05ce0,Yes,https://www.notion.so/kaedea/MarkDown-Test-Page-9a873436a8b54f6a9b8ec1be725548a4,kidhaibara@gmail.com,020-00000000 3 | 测试_NULL,,,,,,,No,,, -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4/Table f802ebd06ae241c989da3c1c66042ea0/测试 0bf98d3a244c4e7cae082693ddb01825.md: -------------------------------------------------------------------------------- 1 | # 测试 2 | 3 | CheckBox: Yes 4 | Date: May 1, 2021 12:00 AM 5 | Email: kidhaibara@gmail.com 6 | Files: https://www.notion.so/a88e9b9088654cfbbdf9c614f5a05ce0 7 | MultiSelect: MULTI_SELECT_ITEM_02, Multi_SELECT_ITEM_01 8 | Number: 123456 9 | Persson: Kaede Akatsuki 10 | Phone: 020-00000000 11 | Select: Select_ITEM_01 12 | Url: https://www.notion.so/kaedea/MarkDown-Test-Page-9a873436a8b54f6a9b8ec1be725548a4 13 | 14 | hello -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4/Table f802ebd06ae241c989da3c1c66042ea0/测试_NULL 0d231330e5314eacb145e8e5f1dadad4.md: -------------------------------------------------------------------------------- 1 | # 测试_NULL 2 | 3 | CheckBox: No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4/kiminonaha_tenkinoko_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/MarkDown Test Page 9a873436a8b54f6a9b8ec1be725548a4/kiminonaha_tenkinoko_2.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown CN-EN Concat Format 4be67947e23843ad81b007bd2ee41c17.md: -------------------------------------------------------------------------------- 1 | # NotionDown CN-EN Concat Format 2 | 3 | ## CN-EN Concat Text Formatting 4 | 5 | Copycat sample text of [https://github.com/vinta/pangu.py](https://github.com/vinta/pangu.py). 6 | 7 | 當你凝視著bug,bug也凝視著你 8 | 9 | 請使用uname -m指令來檢查你的Linux作業系統是32位元或是[敏感词已被屏蔽]位元 10 | 請使用 uname -m 指令來檢查你的 Linux 作業系統是 32 位元或是 [敏感词已被屏蔽] 位元 11 | 12 | 為什麼小明有問題都不Google?因為他有Bing 13 | 為什麼小明有問題都不 Google?因為他有 Bing 14 | 15 | 未來的某一天,Gmail配備的AI可能會得出一個結論:想要消滅垃圾郵件最好的辦法就是消滅人類 16 | 未來的某一天,Gmail 配備的 AI 可能會得出一個結論:想要消滅垃圾郵件最好的辦法就是消滅人類 17 | 18 | 心裡想的是Microservice,手裡做的是Distributed Monolith 19 | 心裡想的是 Microservice,手裡做的是 Distributed Monolith 20 | 21 | 你從什麼時候開始產生了我沒使用Monkey Patch的錯覺? 22 | 你從什麼時候開始產生了我沒使用 Monkey Patch 的錯覺? -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448.md: -------------------------------------------------------------------------------- 1 | # NotionDown Custom Config 2 | 3 | # NotionDown Custom Configurations 4 | 5 | This page shows how to run NotionDown with custom args, and how to configure custom page properties. 6 | 7 | ## Basic Config 8 | 9 | Run `notion-down/main.py` with args: 10 | 11 | ```bash 12 | PYTHONPATH=./ python main.py \ 13 | --blog_url \ 14 | --token_v2 15 | ``` 16 | 17 | Run `notion-down/main.py` with config file: 18 | 19 | ```bash 20 | xxx --config_file '.config_file.json' 21 | 22 | # cat '.config_file.json' 23 | { 24 | "debuggable": false, 25 | "channels": [ 26 | "default" 27 | ], 28 | "page_titles": [ 29 | "all" 30 | ], 31 | "token_v2": "xxx", 32 | "blog_url": "yyy" 33 | } 34 | ``` 35 | 36 | Set `notion token` at SysEnv: 37 | 38 | ```bash 39 | # Only token is supported to be configured at SysEnv. 40 | export NOTION_TOKEN_V2= 41 | ``` 42 | 43 | Your can configure notion-down args by cli-args, config_file or SysEnv parameters with the following override priority. 44 | 45 | Priority: cli args > config_file > SysEnv parameters > NotionDown default 46 | 47 | ## Configure MD Files for HEXO Source 48 | 49 | `WIP` 50 | 51 | ## Configure Output Text Inspections 52 | 53 | ### Chinese-English Concat Separation 54 | 55 | Install `pangu` module to enable cn-en concat separation optimize. 56 | 57 | ```bash 58 | pip install pangu 59 | ``` 60 | 61 | ### Chinese Spelling Error Check 62 | 63 | Add arg `channels` with "SpellInspect" config to enable SpellingError check. 64 | 65 | ```bash 66 | xxx --channels xxx|yyy|SpellInspect 67 | ``` 68 | 69 | ## NotionDown Arguments Description 70 | 71 | For now, NotionDown support page properties as the following: 72 | 73 | [Untitled](NotionDown%20Custom%20Config%203c21bc204f0b48c794ff86c6f38fe448/Untitled%20Database%205e7e3401a02a4f13bc291a22870ebaa7.csv) 74 | 75 | ## NotionDown Page Properties Config 76 | 77 | NotionDown offers several custom properties to configure how markdown file generated. 78 | 79 | For example, add a following `Plain Text` code block in your notion page to get your customization. 80 | 81 | ``` 82 | [notion-down-properties] 83 | Title = NotionDown Custom Page Properties Support 84 | Date = 2021-05-20 85 | Published = false 86 | Category = NotionDown 87 | Tag = Notion, NotionDown 88 | FileLocate = 89 | FileName = notiondown_custom_configs 90 | ``` 91 | 92 | For now, NotionDown support page properties as the following: 93 | 94 | [Untitled](NotionDown%20Custom%20Config%203c21bc204f0b48c794ff86c6f38fe448/Untitled%20Database%208818f00cbc684caaa89eb19ac003d4e6.csv) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7.csv: -------------------------------------------------------------------------------- 1 | Name,Example,Desc,Required 2 | debuggable,true or false,Debug mode flag,No 3 | token_v2,,notion_token_v2,Yes 4 | blog_url,https://www.notion.so/kaedea/NotionDown-Posts-Template-f77f3322915a4ab48caa0f2e76e9d733,Notion public posts url needed to read you notion pages,Yes 5 | channels,default|GitHub|Hexo,NotionDown Channels,No 6 | page_titles,Title1|Title2|Title3|...,Pages that want to be handled. Null or 'all' for all pages.,No 7 | config_file,.config_file.json,Json file with configured args,No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/blog_url 7148bb3511c54f1cb1fb94a9a480babf.md: -------------------------------------------------------------------------------- 1 | # blog_url 2 | 3 | Desc: Notion public posts url needed to read you notion pages 4 | Example: https://www.notion.so/kaedea/NotionDown-Posts-Template-f77f3322915a4ab48caa0f2e76e9d733 5 | Required: Yes -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/channels f669a7efd2f647a188d7aea7fe9cf14a.md: -------------------------------------------------------------------------------- 1 | # channels 2 | 3 | Desc: NotionDown Channels 4 | Example: default|GitHub|Hexo 5 | Required: No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/config_file e24099ca0dec47999323140543add9f5.md: -------------------------------------------------------------------------------- 1 | # config_file 2 | 3 | Desc: Json file with configured args 4 | Example: .config_file.json 5 | Required: No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/debuggable a65ea92ffad34abdb93a06932668e13c.md: -------------------------------------------------------------------------------- 1 | # debuggable 2 | 3 | Desc: Debug mode flag 4 | Example: true or false 5 | Required: No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/page_titles 9a0c6bc317444b85bf8777102895b87b.md: -------------------------------------------------------------------------------- 1 | # page_titles 2 | 3 | Desc: Pages that want to be handled. Null or 'all' for all pages. 4 | Example: Title1|Title2|Title3|... 5 | Required: No -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 5e7e3401a02a4f13bc291a22870ebaa7/token_v2 c53bb2c8055949848ee0574dc7cb2ee9.md: -------------------------------------------------------------------------------- 1 | # token_v2 2 | 3 | Desc: notion_token_v2 4 | Required: Yes -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6.csv: -------------------------------------------------------------------------------- 1 | Name,Example,Desc 2 | Title,基于 Notion 的笔记写作和博客分享自动化方案,MD Title 3 | Date,2021-05-20, 4 | Published,false,Draft or not 5 | Category,DevOps, 6 | Tag,"Tag1, Tag2, Tag3, ...", 7 | FileLocate,devops/subpath/...,Relative path 8 | FileName,file-name,Get 'file-name.dm' -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/Category c99ecf42575740c2887675593dcce017.md: -------------------------------------------------------------------------------- 1 | # Category 2 | 3 | Example: DevOps -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/Date 8f5afa6814074f688b42eb3790aa4f6f.md: -------------------------------------------------------------------------------- 1 | # Date 2 | 3 | Example: 2021-05-20 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/FileLocate 244eda6d681246f58f89905827ddada5.md: -------------------------------------------------------------------------------- 1 | # FileLocate 2 | 3 | Desc: Relative path 4 | Example: devops/subpath/... -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/FileName 725e6562ca304f47b009eb756ce9e93a.md: -------------------------------------------------------------------------------- 1 | # FileName 2 | 3 | Desc: Get 'file-name.dm' 4 | Example: file-name -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/Published e310c539ddf74b8b9ec1e2da54961712.md: -------------------------------------------------------------------------------- 1 | # Published 2 | 3 | Desc: Draft or not 4 | Example: false -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/Tag 9762b5a017cf49e8886391c4847d8108.md: -------------------------------------------------------------------------------- 1 | # Tag 2 | 3 | Example: Tag1, Tag2, Tag3, ... -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Custom Config 3c21bc204f0b48c794ff86c6f38fe448/Untitled Database 8818f00cbc684caaa89eb19ac003d4e6/Title b2628f72f7684966bb18d5cd0fd99346.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | Desc: MD Title 4 | Example: 基于 Notion 的笔记写作和博客分享自动化方案 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09.md: -------------------------------------------------------------------------------- 1 | # NotionDown GetTokenV2 2 | 3 | **There are a couple of Notion hacks that I can think of off the top of my head that require a Notion Token v2. The first, is required in order to create a Notion-based website and the other to use [Chilipepper](https://chilipepper.io/), a form embed for Notion. Here is how to find your v2 token.** 4 | 5 | ## **STEP 1** 6 | 7 | Use [Google Chrome](https://www.google.com/chrome/), and sign into your Notion workspace. 8 | 9 | ## **STEP 2** 10 | 11 | Right click anywhere inside the page and select "Inspect." 12 | 13 | ![NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled.png](NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled.png) 14 | 15 | ## **STEP 3** 16 | 17 | Locate "Application" and select "Cookies." Here you should be able to find "token_v2." Copy the property next to it called "Value." 18 | 19 | ![NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled%201.png](NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled%201.png) 20 | 21 | ![NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled%202.png](NotionDown%20GetTokenV2%20f59e921e852345cda9c36340fbd41b09/Untitled%202.png) 22 | 23 | Note that this post is copycat of [this post](https://www.redgregory.com/notion/2020/6/15/9zuzav95gwzwewdu1dspweqbv481s5) and generated by [notion-down](https://github.com/kaedea/notion-down) from [notion.so NotionDown-getNotionTokenV2](https://www.notion.so/kaedea/NotionDown-GetTokenV2-f59e921e852345cda9c36340fbd41b09). -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled 1.png -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled 2.png -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown GetTokenV2 f59e921e852345cda9c36340fbd41b09/Untitled.png -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085.md: -------------------------------------------------------------------------------- 1 | # NotionDown Image Source 2 | 3 | ![NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image.jpg](NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image.jpg) 4 | 5 | Sample Image 6 | 7 | Updated: 2021-05-30 15:42:48 8 | 9 | Image source was replacing to: [https://s3.us-west-2.amazonaws.com/secure.notion-static.com/4b7a305d-5677-4304-9ca9-d7e44be00509/Sample_Image.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210530%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210530T074247Z&X-Amz-Expires=86400&X-Amz-Signature=09135be815c999bb693212773e7913902af8b64973e11df6d5ca17b36aa9988c&X-Amz-SignedHeaders=host](https://www.notion.so/4b7a305d567743049ca9d7e44be00509) 10 | 11 | --- 12 | 13 | ![NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image%201.jpg](NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image%201.jpg) 14 | 15 | Image 2 16 | 17 | ![NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/my_caption.jpg](NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/my_caption.jpg) 18 | 19 | Image 3 20 | 21 | ![NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image%202.jpg](NotionDown%20Image%20Source%206f87d4fe16104af295128fb77e9c5085/Sample_Image%202.jpg) 22 | 23 | Image 4 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image 1.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image 2.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/Sample_Image.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/my_caption.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Image Source 6f87d4fe16104af295128fb77e9c5085/my_caption.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Nested List ee35f5e58d834865bc38f4e01531e98f.md: -------------------------------------------------------------------------------- 1 | # NotionDown Nested List 2 | 3 | ## NumberList 4 | 5 | 1. First item 6 | 2. Second item on a numbered list. 7 | 3. Third item 8 | 1. nested item 9 | 2. nested item 2 10 | 1. nested nested item 11 | 12 | ## BulletList 13 | 14 | - Unordered 15 | - List here 16 | - 3rd item 17 | - Nested item 18 | - Nested item 2 19 | - Nested nested item -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Obfuscated Blocks 25959a72e55041d6aed69f90226fa45c.md: -------------------------------------------------------------------------------- 1 | # NotionDown Obfuscated Blocks 2 | 3 | ## Link Obfuscating 4 | 5 | [This is a link](https://respawn.io) 6 | 7 | [This is also a link](https://www.notion.so/kaedea/NotionDown-Obfusing-Blocks-25959a72e55041d6aed69f90226fa45c) 8 | 9 | [This is also a link]([https://www.notion.so/kaedea/MarkDown-Test-Page-9a873436a8b54f6a9b8ec1be725548a4](MarkDown%20Test%20Page%209a873436a8b54f6a9b8ec1be725548a4.md)) 10 | 11 | ## Image Obfuscating 12 | 13 | ![NotionDown%20Obfuscated%20Blocks%2025959a72e55041d6aed69f90226fa45c/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg](NotionDown%20Obfuscated%20Blocks%2025959a72e55041d6aed69f90226fa45c/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg) 14 | 15 | This is image 16 | 17 | ![This is also an image](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/bfcde5f2-47ab-426a-a06d-b1cea91781f4/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210515%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210515T101250Z&X-Amz-Expires=86400&X-Amz-Signature=4082b5f128410b128d2da953b8f5b4b719ab1925eb5b6959b66415b0e39492bb&X-Amz-SignedHeaders=host) 18 | 19 | ![This is also an image]([https://s3.us-west-2.amazonaws.com/secure.notion-static.com/bfcde5f2-47ab-426a-a06d-b1cea91781f4/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210515%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210515T101250Z&X-Amz-Expires=86400&X-Amz-Signature=4082b5f128410b128d2da953b8f5b4b719ab1925eb5b6959b66415b0e39492bb&X-Amz-SignedHeaders=host](https://www.notion.so/bfcde5f247ab426aa06db1cea91781f4)) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Obfuscated Blocks 25959a72e55041d6aed69f90226fa45c/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Obfuscated Blocks 25959a72e55041d6aed69f90226fa45c/mmexportd44a4a78d543429542df4e038acffc84_1619870561717.jpeg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774.md: -------------------------------------------------------------------------------- 1 | # NotionDown Properties 2 | 3 | ``` 4 | [notion-down-properties] 5 | Title = NotionDown Custom Properties Support 6 | Date = 2021-05-20 7 | Published = false 8 | Category = NotionDown 9 | Tag = Notion, NotionDown 10 | FileLocate = custom 11 | FileName = notion-down-properties 12 | ``` 13 | 14 | This page is showing how to configure custom properties for NotionDown. 15 | 16 | For now, NotionDown support properties as the following: 17 | 18 | [Untitled](NotionDown%20Properties%207c43bf2594954ec9816eede682d8e774/Untitled%20Database%209169514bc3d14895b9d04c303b43e90c.csv) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c.csv: -------------------------------------------------------------------------------- 1 | Name,Example,Desc 2 | Title,基于 Notion 的笔记写作和博客分享自动化方案,MD Title 3 | Date,2021-05-20, 4 | Published,false,Draft or not 5 | Category,DevOps, 6 | Tag,"Tag1, Tag2, Tag3, ...", 7 | FileLocate,devops/subpath/...,Relative path 8 | FileName,file-name,Get 'file-name.dm' -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/Category f2c5fb07a7714a4b8d2666595cf8f80f.md: -------------------------------------------------------------------------------- 1 | # Category 2 | 3 | Example: DevOps -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/Date 0d0cb3cfcf4a4eeab72d681d28f07e51.md: -------------------------------------------------------------------------------- 1 | # Date 2 | 3 | Example: 2021-05-20 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/FileLocate 8c09075d24dd410fb1b792d46fc0e9fd.md: -------------------------------------------------------------------------------- 1 | # FileLocate 2 | 3 | Desc: Relative path 4 | Example: devops/subpath/... -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/FileName 7705b604933449d69bea9e3df0373ba2.md: -------------------------------------------------------------------------------- 1 | # FileName 2 | 3 | Desc: Get 'file-name.dm' 4 | Example: file-name -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/Published 9b231c2452a648819bc0db771885e910.md: -------------------------------------------------------------------------------- 1 | # Published 2 | 3 | Desc: Draft or not 4 | Example: false -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/Tag 72f6b19212bb40b5aa42f45be0f97a38.md: -------------------------------------------------------------------------------- 1 | # Tag 2 | 3 | Example: Tag1, Tag2, Tag3, ... -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Properties 7c43bf2594954ec9816eede682d8e774/Untitled Database 9169514bc3d14895b9d04c303b43e90c/Title 98ac1e50846848a4b053d3870a2bde19.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | Desc: MD Title 4 | Example: 基于 Notion 的笔记写作和博客分享自动化方案 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Pullquote Blocks 5e84918760af4ff78fd8ade884955189.md: -------------------------------------------------------------------------------- 1 | # NotionDown Pullquote Blocks 2 | 3 | ## Pullquoted Image 4 | 5 | ![NotionDown%20Pullquote%20Blocks%205e84918760af4ff78fd8ade884955189/kiminonaha_tenkinoko_2.jpg](NotionDown%20Pullquote%20Blocks%205e84918760af4ff78fd8ade884955189/kiminonaha_tenkinoko_2.jpg) 6 | 7 | 善我王上魚、產生資西員合兒臉趣論。畫衣生這著爸毛親可時,安程幾?合學作。觀經而作建。都非子作這!法如言子你關!手師也。 8 | 9 | 以也座論頭室業放。要車時地變此親不老高小是統習直麼調未,行年香一? 10 | 11 | 就竟在,是我童示讓利分和異種百路關母信過明驗有個歷洋中前合著區亮風值新底車有正結,進快保的行戰從:弟除文辦條國備當來際年每小腳識世可的的外的廣下歌洲保輪市果底天影;全氣具些回童但倒影發狀在示,數上學大法很,如要我……月品大供這起服滿老?應學傳者國:山式排只不之然清同關;細車是!停屋常間又,資畫領生,相們制在?公別的人寫教資夠。資再我我!只臉夫藝量不路政吃息緊回力之;兒足灣電空時局我怎初安。意今一子區首者微陸現際安除發連由子由而走學體區園我車當會,經時取頭,嚴了新科同?很夫營動通打,出和導一樂,查旅他。坐是收外子發物北看蘭戰坐車身做可來。道就學務。 12 | 13 | 國新故。 14 | 15 | > 工步他始能詩的,裝進分星海演意學值例道……於財型目古香亮自和這乎?化經溫詩。只賽嚴大一主價世哥受的沒有中年即病行金拉麼河。主小路了種就小為廣不? 16 | > 17 | 18 | ## Pullquoted Text 19 | 20 | 私は昨日ついにその助力家というのの上よりするたなけれ。最も今をお話団はちょうどこの前後なかろでくらいに困りがいるたをは帰着考えたなかって、そうにもするでうたらない。がたを知っないはずも同時に九月をいよいよたありた。 21 | 22 | もっと槙さんにぼんやり金少し説明にえた自分大した人私か影響にというお関係たうませないが、この次第も私か兄具合に使うて、槙さんののに当人のあなたにさぞご意味と行くて私個人が小尊敬を聴いように同時に同反抗に集っだうて、いよいよまず相当へあっうからいだ事をしでなけれ。 23 | 24 | > それでそれでもご時日をしはずはたったいやと突き抜けるますて、その元がは行ったてという獄を尽すていけですた。 25 | > 26 | 27 | この中道具の日その学校はあなたごろがすまなりかとネルソンさんの考えるですん、辺の事実ないというご盲従ありたですと、爺さんのためが薬缶が結果までの箸の当時してならて、多少の十月にためからそういう上からとにかくしましないと触れべきものたで、ないうですと多少お人達したのでたた。 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Pullquote Blocks 5e84918760af4ff78fd8ade884955189/kiminonaha_tenkinoko_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Pullquote Blocks 5e84918760af4ff78fd8ade884955189/kiminonaha_tenkinoko_2.jpg -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown README d3463f3d398743879d663caf87efa029.md: -------------------------------------------------------------------------------- 1 | # NotionDown README 2 | 3 | # Notion Down 4 | 5 | [🇨🇳](https://www.kaedea.com/2021/05/01/devops/project-notion-down/) [🇯🇵](https://preview.kaedea.com/2021/05/01/devops/project-notion-down-jp/) 6 | 7 | ![https://circleci.com/gh/kaedea/notion-down.svg?style=shield&circle-token=9f4dc656e94d8deccd362e52400c96e709c7e8b3&keep-url-source=true](https://circleci.com/gh/kaedea/notion-down.svg?style=shield&circle-token=9f4dc656e94d8deccd362e52400c96e709c7e8b3&keep-url-source=true) 8 | 9 | [Notion Down](https://github.com/kaedea/notion-down), python tools that convert Notion blog pages into Markdown files, along with integration to build static webpages such as Hexo. Its inspiration and goal is to **avoid separation of writing** by keep writing drafts or posts within [notion.so](http://notion.so) and then publish them into MD webpages automatically. 10 | 11 | ## Examples 12 | 13 | [kaedea.com](http://www.kaedea.com) 14 | 15 | [hexo.kaedea.com](http://hexo.kaedea.com) 16 | 17 | [基于 Notion 的笔记写作和博客分享自动化方案](https://www.kaedea.com/2021/05/20/devops/notion-to-markdown-file-automating-solution/) 18 | 19 | ## Features 20 | 21 | What can NotionDown do now: 22 | 23 | - Notion pages to MarkDown files 24 | - ~~Basic Notion PageBlocks parsing~~ 25 | - ~~Notion images refer & download~~ 26 | - ~~Notion nested list blocks~~ 27 | - ~~Notion obfuscated-links parsing~~ 28 | - ~~Notion table block (Collection)~~ 29 | - Notion subpage / alias link parsing 30 | - Advanced Notion PageBlocks support 31 | - ~~Pullquote Blocks (Notion ColumnList)~~ 32 | - Image source replacing 33 | - ~~Replace notion image url with image file~~ 34 | - Replace notion image url with other CDN urls 35 | - Notion page embed blocks 36 | - Writing optimized integration 37 | - ~~Noton custom `ShortCode` blocks that control parametered MD files generating~~ 38 | - ~~Mixed CN-EN text separation format~~ ([by pangu](https://github.com/vinta/pangu)) 39 | - ~~Spelling inspect~~ (by [pycorrector](https://github.com/shibing624/pycorrector)) 40 | - HEXO Integration 41 | - ~~HEXO page properties config~~ 42 | - ~~HEXO generate~~ 43 | - HEXO tags plugin 44 | - PyPI Publish 45 | - Notion APIs 46 | - ~~notion-py (3rd party)~~ 47 | - notion-sdk (official) 48 | 49 | ## Hot It Works 50 | 51 | ![NotionDown%20README%20d3463f3d398743879d663caf87efa029/NotionDown.png](NotionDown%20README%20d3463f3d398743879d663caf87efa029/NotionDown.png) 52 | 53 | NotionDown Workflows 54 | 55 | NotionDown read Notion pages data using [notion-py](https://github.com/jamalex/notion-py), and then write pages into MD files. 56 | 57 | ### Basic usage 58 | 59 | > notion-down >> Notion APIs (notion-py) >> Notion pages data >> generating MD files 60 | > 61 | 62 | ### Advanced usage 63 | 64 | > WebHook >> notion-down >> Notion APIs (notion-py) >> Notion pages data >> generating MD files >> Copy into Hexo source >> generating webpages >> push to GitHub pages 65 | > 66 | 67 | ## Getting Started 68 | 69 | ### Prepare 70 | 71 | To get started with NotionDown, you should: 72 | 73 | 1. Prepare `notion_token_v2`. 74 | 2. Prepare `public notion blog_url` as root post for NotionDown to get the pages you want to handle. 75 | 3. Run `notion-down/main.py` with your configs. 76 | 77 | Check [here](https://github.com/kaedea/notion-down/blob/master/dist/parse_readme/notiondown_gettokenv2.md) to get `notion_token_v2`. 78 | 79 | Duplicate [NotionDown Posts Template](../NotionDown%20Posts%20Template%20f77f3322915a4ab48caa0f2e76e9d733.md) to your own notion and take it as `blog_url` (or you can just use your existing blog post url). Note that, for now the root page should be public as well as placed in root path of notion workspace. 80 | 81 | ### Run NotionDown 82 | 83 | Basically just run `notion-down/main.py` : 84 | 85 | ```bash 86 | # Run with cli cmd 87 | PYTHONPATH=./ python main.py \ 88 | --blog_url \ 89 | --token_v2 90 | 91 | # or 92 | PYTHONPATH=./ python main.py \ 93 | --config_file '.config_file.json' 94 | 95 | # Your can configure notion-down args by cli-args, config_file or SysEnv parameters 96 | # Priority: cli args > config_file > SysEnv parameters > NotionDown default 97 | ``` 98 | 99 | For custom configurations in details, see [Custom Configurations](https://github.com/kaedea/notion-down/blob/master/dist/parse_readme/notiondown_custom_configs.md). 100 | 101 | Also check the following procedures as showcase usages for NotionDown. 102 | 103 | ### CI Build Script 104 | 105 | See building script at `/.circleci/config.yaml`. 106 | 107 | - `test-build-readme`: CircleCI jobs generating README for this repo. 108 | - `test-build-hexo`: CircleCI jobs generating Hexo posts for [https://github.com/kaedea/notion-down-hexo-showcase](https://github.com/kaedea/notion-down-hexo-showcase). 109 | - `test-run-pycorrector`: CircleCI jobs that executing spelling check for the test posts. 110 | 111 | ### Showcase Jobs 112 | 113 | See the usage showcase jobs at [/jobs](/jobs), and jobs outputs at [/dist](/dist). 114 | 115 | - [README generating](/jobs/parse_readme/) 116 | - [Notion sample post generating](/jobs/parse_sample_posts/) 117 | - [HEXO public generating](/jobs/parse_sample_posts_for_hexo/) 118 | - Notion image page source replacing (WIP) 119 | 120 | ### UnitTest Examples 121 | 122 | See unittest cases at `test/`. 123 | 124 | --- 125 | 126 | This page is generated by [notion-down](https://github.com/kaedea/notion-down) from [notion.so NotionDown-README](https://www.notion.so/kaedea/NotionDown-README-d3463f3d398743879d663caf87efa029). -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown README d3463f3d398743879d663caf87efa029/NotionDown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown README d3463f3d398743879d663caf87efa029/NotionDown.png -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown ShortCode daa1568f28a64afebdc57c43b1401926.md: -------------------------------------------------------------------------------- 1 | # NotionDown ShortCode 2 | 3 | ## Channel ShortCode 4 | 5 | ```html 6 | ShortCode: 7 | 11 | 12 | Channel ShortCode: 13 | 16 | ``` 17 | 18 | 25 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Spelling Inspect a6721bfa57da451d8a328fa1f3418969.md: -------------------------------------------------------------------------------- 1 | # NotionDown Spelling Inspect 2 | 3 | 真麻烦你了。希望你们好好的跳无。 4 | 5 | 少先队员因该为老人让坐。 6 | 7 | 机七学习是人工智能领遇最能体现智能的一个分知。 8 | 9 | 一只小鱼船浮在平净的河面上。 10 | 11 | 我的家乡是有明的渔米之乡。 12 | 13 | 真麻烦你了。希望你们好好的跳无。少先队员因该为老人让坐。机七学习是人工智能领遇最能体现智能的一个分知。一只小鱼船浮在平净的河面上。我的家乡是有明的渔米之乡。 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818.md: -------------------------------------------------------------------------------- 1 | # NotionDown Table 2 | 3 | [Table Name](NotionDown%20Table%20ff82c0e4b9504eb3b0b82549cfb18818/Table%20Name%200ec1aaf3a8f141f7be0ed8da40b1ae52.csv) -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818/Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52.csv: -------------------------------------------------------------------------------- 1 | Title,Number,Select,MultiSelect,Date,Persson,Files,CheckBox,Url,Email,Phone 2 | 测试,123456,Select_ITEM_01,"MULTI_SELECT_ITEM_02, Multi_SELECT_ITEM_01","May 1, 2021 12:00 AM",Kaede Akatsuki,https://www.notion.so/a88e9b9088654cfbbdf9c614f5a05ce0,Yes,https://www.notion.so/kaedea/MarkDown-Test-Page-9a873436a8b54f6a9b8ec1be725548a4,kidhaibara@gmail.com,020-00000000 3 | 测试_NULL,,,,,,,No,,, 4 | ,,,MULTI_SELECT_ITEM_02,,,,No,,, -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818/Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52/Untitled c8a900b2bb334ff5a7d9bdd0d3dc2a27.md: -------------------------------------------------------------------------------- 1 | # Untitled 2 | 3 | CheckBox: No 4 | MultiSelect: MULTI_SELECT_ITEM_02 -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818/Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52/测试 919e70a199b74714a9b82083820ac674.md: -------------------------------------------------------------------------------- 1 | # 测试 2 | 3 | CheckBox: Yes 4 | Date: May 1, 2021 12:00 AM 5 | Email: kidhaibara@gmail.com 6 | Files: https://www.notion.so/a88e9b9088654cfbbdf9c614f5a05ce0 7 | MultiSelect: MULTI_SELECT_ITEM_02, Multi_SELECT_ITEM_01 8 | Number: 123456 9 | Persson: Kaede Akatsuki 10 | Phone: 020-00000000 11 | Select: Select_ITEM_01 12 | Url: https://www.notion.so/kaedea/MarkDown-Test-Page-9a873436a8b54f6a9b8ec1be725548a4 13 | 14 | hello -------------------------------------------------------------------------------- /dist/notion-exported/NotionDown Sample 440de7dca89840b6b3bab13d2aa92a34/NotionDown Table ff82c0e4b9504eb3b0b82549cfb18818/Table Name 0ec1aaf3a8f141f7be0ed8da40b1ae52/测试_NULL b906a905c34e4fefbea971e923e31d76.md: -------------------------------------------------------------------------------- 1 | # 测试_NULL 2 | 3 | CheckBox: No -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from notion_token import NotionToken 3 | from utils.config import Config 4 | from notion_backup import NotionUp 5 | 6 | 7 | def start(): 8 | print("Run with configs:") 9 | print("config = {}".format(Config.to_string())) 10 | 11 | if not Config.username() and not Config.password() and not Config.token_v2(): 12 | raise Exception('username|password or token_v2 should be presented!') 13 | 14 | if Config.username() and Config.password(): 15 | new_token = NotionToken.getNotionToken(Config.username(), Config.password()) 16 | if len(new_token) > 0: 17 | print("Use new token fetched by username-password.") 18 | Config.set_token_v2(new_token) 19 | 20 | if Config.action() not in ['all', 'export', 'unzip']: 21 | raise Exception('unknown action: {}'.format(Config.action())) 22 | 23 | if Config.action() in ['all', 'export']: 24 | # get backup file 25 | zips = NotionUp.backup() 26 | Config.set_zip_files(zips) 27 | 28 | if Config.action() in ['all', 'unzip']: 29 | # unzip 30 | NotionUp.unzip() 31 | # archive files 32 | 33 | 34 | # Cli cmd example: 35 | # python main.py \ 36 | # --action 37 | # --username # Only when token_v2 is not presented 38 | # --password # Only when token_v2 is not presented 39 | # --token_v2 40 | # or 41 | # python main.py \ 42 | # --config_file '.config_file.json' 43 | # 44 | if __name__ == '__main__': 45 | Config.parse_configs() 46 | Config.check_required_args() 47 | Config.check_required_modules() 48 | 49 | print('\nHello, NotionDown!\n') 50 | start() 51 | -------------------------------------------------------------------------------- /notion_backup.py: -------------------------------------------------------------------------------- 1 | import json 2 | import shutil 3 | import time 4 | import zipfile 5 | from pathlib import Path 6 | from typing import List 7 | 8 | import requests 9 | from slugify import slugify 10 | 11 | from utils.config import Config 12 | from utils.utils import FileUtils 13 | 14 | 15 | class NotionUp: 16 | 17 | def __init__(self) -> None: 18 | super().__init__() 19 | 20 | @staticmethod 21 | def exportTask(spaceId): 22 | return { 23 | 'task': { 24 | 'eventName': "exportSpace", 25 | 'request': { 26 | 'spaceId': spaceId, 27 | 'exportOptions': { 28 | 'exportType': 'markdown', 29 | 'timeZone': Config.notion_timezone(), 30 | 'locale': Config.notion_locale(), 31 | } 32 | } 33 | } 34 | } 35 | 36 | @staticmethod 37 | def requestPost(endpoint: str, params: object): 38 | response = requests.post( 39 | f'{Config.notion_api()}/{endpoint}', 40 | data=json.dumps(params).encode('utf8'), 41 | headers={ 42 | 'content-type': 'application/json', 43 | 'cookie': f'token_v2={Config.token_v2()}; ' 44 | }, 45 | ) 46 | 47 | return response.json() 48 | 49 | @staticmethod 50 | def getUserContent(): 51 | return NotionUp.requestPost("loadUserContent", {})["recordMap"] 52 | 53 | @staticmethod 54 | def waitForExportedUrl(taskId): 55 | print('Polling for export task: {}'.format(taskId)) 56 | while True: 57 | res = NotionUp.requestPost('getTasks', {'taskIds': [taskId]}) 58 | tasks = res.get('results') 59 | task = next(t for t in tasks if t['id'] == taskId) 60 | if task['state'] == 'success': 61 | url = task['status']['exportURL'] 62 | print('\n' + url) 63 | break 64 | else: 65 | print('.', end="", flush=True) 66 | time.sleep(10) 67 | return url 68 | 69 | @staticmethod 70 | def downloadFile(url, filename) -> str: 71 | file = FileUtils.new_file(Config.output(), filename) 72 | FileUtils.create_file(file) 73 | with requests.get(url, stream=True) as r: 74 | with open(file, 'wb') as f: 75 | shutil.copyfileobj(r.raw, f) 76 | return file 77 | 78 | @staticmethod 79 | def backup() -> List[str]: 80 | zips = [] 81 | # tokens 82 | userContent = NotionUp.getUserContent() 83 | userId = list(userContent["notion_user"].keys())[0] 84 | print(f"User id: {userId}") 85 | 86 | # list spaces 87 | spaces = [(space_id, space_details["value"]["name"]) for (space_id, space_details) in userContent["space"].items()] 88 | print("Available spaces total: {}".format(len(spaces))) 89 | for (spaceId, spaceName) in spaces: 90 | print(f"\nexport space: {spaceId}, {spaceName}") 91 | # request export task 92 | taskId = NotionUp.requestPost('enqueueTask', NotionUp.exportTask(spaceId)).get('taskId') 93 | # get exported file url and download 94 | url = NotionUp.waitForExportedUrl(taskId) 95 | filename = slugify(f'{spaceName}-{spaceId}') + '.zip' 96 | print('download exported zip: {}, {}'.format(url, filename)) 97 | filePath = NotionUp.downloadFile(url, filename) 98 | zips.append(filePath) 99 | break 100 | return zips 101 | 102 | @staticmethod 103 | def unzipFile(filePath: str, saveDir: str = None): 104 | try: 105 | if not saveDir: 106 | saveDir = FileUtils.new_file(Config.output(), Path(filePath).name.replace('.zip', '')) 107 | FileUtils.clean_dir(saveDir) 108 | 109 | file = zipfile.ZipFile(filePath) 110 | file.extractall(saveDir) 111 | file.close() 112 | return saveDir 113 | except Exception as e: 114 | print(f'{filePath} unzip fail,{str(e)}') 115 | 116 | @staticmethod 117 | def unzip(): 118 | for file in Config.zip_files(): 119 | print('unzip exported zip: {}'.format(file)) 120 | NotionUp.unzipFile(file) 121 | 122 | -------------------------------------------------------------------------------- /notion_token.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | 5 | 6 | class NotionToken: 7 | 8 | def __init__(self) -> None: 9 | super().__init__() 10 | 11 | @staticmethod 12 | def getNotionToken(email, password): 13 | if not email: 14 | raise Exception('Email is not presented!') 15 | if not password: 16 | raise Exception('Password is not presented!') 17 | 18 | loginData = { 19 | 'email': email, 20 | 'password': password 21 | } 22 | headers = { 23 | # Notion obviously check this as some kind of (bad) test of CSRF 24 | 'host': 'www.notion.so' 25 | } 26 | response = requests.post( 27 | 'https://notion.so/api/v3/loginWithEmail', 28 | json=loginData, 29 | headers=headers 30 | ) 31 | response.raise_for_status() 32 | return response.cookies['token_v2'] 33 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/test/__init__.py -------------------------------------------------------------------------------- /test/notion_client_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from notion_token import NotionToken 5 | 6 | 7 | class NotionClientTest(unittest.TestCase): 8 | 9 | def test_check_env(self): 10 | self.assertTrue("NOTION_USERNAME" in os.environ) 11 | self.assertTrue("NOTION_PASSWORD" in os.environ) 12 | 13 | def test_get_token_v2(self): 14 | self.test_check_env() 15 | token = NotionToken.getNotionToken( 16 | os.environ['NOTION_USERNAME'], 17 | os.environ['NOTION_PASSWORD'] 18 | ) 19 | self.assertTrue(len(token) > 0, "get token: {}".format(token)) 20 | 21 | 22 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedea/notion-up/a36ef1048399179562ecb966f809b73276336644/utils/__init__.py -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | 6 | from utils.utils import Utils 7 | 8 | DEFAULT_ARGS = { 9 | 'config_file': None, 10 | 'debuggable': True, 11 | 'workspace': Utils.get_workspace(), 12 | 'output': os.path.join(Utils.get_workspace(), "build"), 13 | 'notion_api': 'https://www.notion.so/api/v3', 14 | 'notion_locale': 'en', 15 | 'notion_timezone': 'Asia/Shanghai', 16 | 'username': None, 17 | 'password': None, 18 | 'token_v2': None, 19 | 'action': "all", # all | export | unzip 20 | 'zip_files': [], 21 | } 22 | SYS_ENV_MAP = { 23 | 'token_v2': "NOTION_TOKEN_V2", 24 | 'username': "NOTION_USERNAME", 25 | 'password': "NOTION_PASSWORD", 26 | } 27 | REQUIRED_ARGS = [ 28 | 'action', 29 | ] 30 | PRIVATE_ARGS = [ 31 | 'username', 32 | 'password', 33 | 'token_v2', 34 | ] 35 | REQUIRED_MODULES = [ 36 | ] 37 | PROPERTIES = {} 38 | 39 | 40 | class Config: 41 | 42 | @staticmethod 43 | def get(key, default=None): 44 | if key in PROPERTIES: 45 | return PROPERTIES[key] 46 | return default 47 | 48 | @staticmethod 49 | def set(key, value): 50 | PROPERTIES[key] = value 51 | 52 | @staticmethod 53 | def load_sys_env(): 54 | for key, value in SYS_ENV_MAP.items(): 55 | if os.environ.get(value) is not None: 56 | Config.set(key, os.environ.get(value)) 57 | 58 | @staticmethod 59 | def load_config_file(file_path): 60 | if os.path.exists(file_path): 61 | json_obj = Utils.parse_json(file_path) 62 | if type(json_obj) is not dict: 63 | raise Exception("config file should be dict:\n{}".format(json_obj)) 64 | for k in json_obj: 65 | v = json_obj[k] 66 | PROPERTIES[k] = v 67 | else: 68 | raise Exception("config file not found: {}".format(file_path)) 69 | 70 | @staticmethod 71 | def define_getter(name, prefix=''): 72 | def get_block(value=None): 73 | return Config.get(name, value) 74 | setattr(Config, prefix + name, get_block) 75 | 76 | @staticmethod 77 | def define_setter(name): 78 | def set_block(value=None): 79 | if value: 80 | Config.set(name, value) 81 | return Config.get(name) 82 | setattr(Config, 'set_' + name, set_block) 83 | 84 | @staticmethod 85 | def parse_configs(): 86 | ArgsParser.parse() 87 | 88 | @staticmethod 89 | def check_required_args(): 90 | for item in REQUIRED_ARGS: 91 | if item not in PROPERTIES or PROPERTIES[item] is None: 92 | raise Exception('\'{}\' is null or not presented, configure it in sys_env | config_file | cli_args !'.format(item)) 93 | 94 | @staticmethod 95 | def check_required_modules(): 96 | for module in REQUIRED_MODULES: 97 | if not Utils.check_module_installed(module): 98 | raise Exception("{} not installed, pls exec 'pip install {}' first!".format(module, module)) 99 | 100 | @staticmethod 101 | def to_string(): 102 | args = dict(PROPERTIES) 103 | for key in PRIVATE_ARGS: 104 | args[key] = "*" * 10 105 | return json.dumps(args, indent=2) 106 | 107 | 108 | 109 | class ArgsParser: 110 | 111 | @staticmethod 112 | def parse(): 113 | # 1. Config getter & setter 114 | for key in DEFAULT_ARGS.keys(): 115 | Config.define_getter(key) 116 | Config.define_getter(key, "get_") 117 | Config.define_setter(key) 118 | 119 | # 2. Load default values (lowest priority) 120 | for key in DEFAULT_ARGS.keys(): 121 | if DEFAULT_ARGS[key]: 122 | Config.set(key, DEFAULT_ARGS[key]) 123 | 124 | # 3. load configs from SysEnv parameters 125 | Config.load_sys_env() 126 | 127 | # 4. Load configs from cli input (highest priority) 128 | import argparse 129 | parser = argparse.ArgumentParser(description='Process running args.') 130 | for key in DEFAULT_ARGS.keys(): 131 | parser.add_argument("--{}".format(key), nargs='?', default=None) 132 | 133 | if Utils.is_unittest(): 134 | # don NOT parse args when running from unittest 135 | cli_args = {key: None for key in DEFAULT_ARGS.keys()} 136 | else: 137 | cli_args = parser.parse_args() 138 | 139 | for key in DEFAULT_ARGS.keys(): 140 | input_value = cli_args[key] if type(cli_args) is dict else getattr(cli_args, key) 141 | if input_value: 142 | # load configs from output config_file 143 | if key == 'config_file': 144 | Config.load_config_file(input_value) 145 | # parse cli input args 146 | if type(DEFAULT_ARGS[key]) is bool: 147 | Config.set(key, True if str(input_value).lower() == 'true' else False) 148 | elif type(DEFAULT_ARGS[key]) is int: 149 | Config.set(key, int(input_value)) 150 | elif type(DEFAULT_ARGS[key]) is list: 151 | # list arg divided by '|' 152 | Config.set(key, [it.strip() for it in str(input_value).split("|")]) 153 | else: 154 | Config.set(key, input_value) 155 | 156 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | import shutil 6 | import subprocess 7 | import sys 8 | import tempfile 9 | from pathlib import Path 10 | 11 | import pkg_resources 12 | 13 | 14 | class Utils: 15 | 16 | def __init__(self): 17 | pass 18 | 19 | @staticmethod 20 | def pwd(): 21 | return os.getcwd() 22 | 23 | @staticmethod 24 | def get_workspace(): 25 | return str(Path(os.path.dirname(os.path.realpath(__file__))).parent.absolute()) 26 | 27 | @staticmethod 28 | def get_temp_dir(): 29 | return tempfile.gettempdir() 30 | 31 | @staticmethod 32 | def is_git_directory(path='.'): 33 | return subprocess.call( 34 | ['git', '-C', path, 'status'], 35 | stderr=subprocess.STDOUT, 36 | stdout=open(os.devnull, 'w') 37 | ) == 0 38 | 39 | @staticmethod 40 | def find(array, predicate): 41 | finds = [it for it in array if predicate(it)] 42 | return finds 43 | 44 | @staticmethod 45 | def find_one(array, predicate): 46 | finds = [it for it in array if predicate(it)] 47 | if finds: 48 | return finds[0] 49 | return finds 50 | 51 | @staticmethod 52 | def parse_json(file_path): 53 | with open(file_path) as f: 54 | data = json.load(f) 55 | return data 56 | 57 | @staticmethod 58 | def safe_getattr(obj, name, default_value=None): 59 | if name in obj: 60 | return obj[name] 61 | if hasattr(obj, name): 62 | return getattr(obj, name) 63 | return default_value 64 | 65 | @staticmethod 66 | def get_props(obj): 67 | return [it for it in dir(obj) if not it.startswith('_')] 68 | 69 | @staticmethod 70 | def parse_bean(bean, my_dict): 71 | for prop in Utils.get_props(bean): 72 | value = Utils.safe_getattr(my_dict, prop) 73 | if value: 74 | setattr(bean, prop, value) 75 | return bean 76 | 77 | @staticmethod 78 | def paging(array, page, limit): 79 | page = int(page) 80 | limit = int(limit) 81 | 82 | if page >= 1 and limit >= 1: 83 | count_str = limit * (page - 1) # 0, 20 84 | count_end = limit * page # 20, 40 85 | count_max = len(array) 86 | if count_end >= count_max: 87 | count_end = count_max 88 | 89 | print(count_str) 90 | print(count_end) 91 | print(count_max) 92 | array = array[count_str:count_end] 93 | 94 | return array 95 | 96 | @staticmethod 97 | def assert_property_presented(item, properties): 98 | props = [] 99 | if isinstance(properties, list): 100 | props = properties 101 | else: 102 | props.append(properties) 103 | 104 | for key in props: 105 | if key not in item: 106 | raise Exception('\'{}\' is not given: {}'.format(key, item)) 107 | 108 | @staticmethod 109 | def check_property_presented(item, properties): 110 | props = [] 111 | if isinstance(properties, list): 112 | props = properties 113 | else: 114 | props.append(properties) 115 | 116 | result = True 117 | for key in props: 118 | if key not in item: 119 | print('\'{}\' is not given: {}'.format(key, item)) 120 | result = False 121 | return result 122 | 123 | @staticmethod 124 | def check_module_installed(name): 125 | try: 126 | pkg_resources.get_distribution(name) 127 | return True 128 | except pkg_resources.DistributionNotFound: 129 | return False 130 | 131 | @staticmethod 132 | def is_unittest(): 133 | return 'unittest' in sys.modules.keys() 134 | 135 | 136 | class FileUtils: 137 | @staticmethod 138 | def new_file(file_dir, file_name): 139 | return os.path.join(file_dir, file_name) 140 | 141 | @staticmethod 142 | def exists(file_path): 143 | return Path(file_path).exists() 144 | 145 | @staticmethod 146 | def create_file(file_path, fore=False): 147 | path = Path(file_path) 148 | if not path.exists(): 149 | path.parent.mkdir(parents=True, exist_ok=True) 150 | path.touch(exist_ok=True) 151 | 152 | if path.is_dir(): 153 | if not fore: 154 | raise Exception("{} is dir".format(file_path)) 155 | shutil.rmtree(path.absolute()) 156 | path.touch(exist_ok=True) 157 | 158 | @staticmethod 159 | def create_dir(file_path, fore=False): 160 | path = Path(file_path) 161 | if not path.exists(): 162 | path.mkdir(parents=True, exist_ok=True) 163 | return 164 | if path.is_file(): 165 | if not fore: 166 | raise Exception("{} is file".format(file_path)) 167 | os.remove(path.absolute()) 168 | path.mkdir(parents=True, exist_ok=True) 169 | 170 | @staticmethod 171 | def clean_dir(file_path, fore=False): 172 | FileUtils.delete_dir(file_path, fore) 173 | FileUtils.create_dir(file_path, fore) 174 | 175 | @staticmethod 176 | def delete_dir(file_path, fore=False): 177 | path = Path(file_path) 178 | if path.is_file(): 179 | if not fore: 180 | raise Exception("{} is file".format(file_path)) 181 | os.remove(path.absolute()) 182 | return 183 | if path.exists(): 184 | shutil.rmtree(file_path) 185 | 186 | @staticmethod 187 | def delete(file_path): 188 | path = Path(file_path) 189 | if path.is_file(): 190 | os.remove(path.absolute()) 191 | return 192 | if path.exists(): 193 | shutil.rmtree(file_path) 194 | 195 | @staticmethod 196 | def write_text(text, file_path, mode="w+"): 197 | with open(file_path, mode) as f: 198 | f.write(text) 199 | 200 | --------------------------------------------------------------------------------