chushihua

This commit is contained in:
zhouhongshuo 2024-08-26 00:08:18 +08:00
commit 3be20f90fc
1210 changed files with 151127 additions and 0 deletions

View File

@ -0,0 +1,49 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-in-docker
{
"name": "Halo Dev Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "20.10",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/java:1": {
"version": "17",
"jdkDistro": "tem"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"Vue.volar",
"vscodevim.vim",
"shengchen.vscode-checkstyle",
"streetsidesoftware.code-spell-checker",
"vscjava.vscode-gradle",
"vmware.vscode-boot-dev-pack",
"vscjava.vscode-java-pack",
"bradlc.vscode-tailwindcss"
]
}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "docker --version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
ui
.github
.git

513
.editorconfig Normal file
View File

@ -0,0 +1,513 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true
ij_smart_tabs = false
ij_wrap_on_typing = false
[*.java]
max_line_length = 100
ij_continuation_indent_size = 4
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = false
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = false
ij_java_align_multiline_parameters_in_calls = false
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = normal
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = normal
ij_java_assignment_wrap = normal
ij_java_binary_operation_sign_on_next_line = true
ij_java_binary_operation_wrap = normal
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 0
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 1
ij_java_block_brace_style = end_of_line
ij_java_block_comment_at_first_column = false
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = normal
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 999
ij_java_class_names_in_javadoc = 1
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_while_brace_force = always
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = false
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_enum_constants_wrap = normal
ij_java_extends_keyword_wrap = normal
ij_java_extends_list_wrap = normal
ij_java_field_annotation_wrap = split_into_lines
ij_java_finally_on_new_line = false
ij_java_for_brace_force = always
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = normal
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = always
ij_java_imports_layout = $*, |, *, |, *
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = true
ij_java_line_comment_at_first_column = false
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = normal
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = normal
ij_java_modifier_list_wrap = false
ij_java_names_count_to_use_import_on_demand = 999
ij_java_new_line_after_lparen_in_record_header = false
ij_java_parameter_annotation_wrap = normal
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = normal
ij_java_rparen_on_new_line_in_record_header = false
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = true
ij_java_space_before_catch_keyword = true
ij_java_space_before_catch_left_brace = true
ij_java_space_before_catch_parentheses = true
ij_java_space_before_class_left_brace = true
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_do_left_brace = true
ij_java_space_before_else_keyword = true
ij_java_space_before_else_left_brace = true
ij_java_space_before_finally_keyword = true
ij_java_space_before_finally_left_brace = true
ij_java_space_before_for_left_brace = true
ij_java_space_before_for_parentheses = true
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = true
ij_java_space_before_if_parentheses = true
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = true
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = true
ij_java_space_before_switch_parentheses = true
ij_java_space_before_synchronized_left_brace = true
ij_java_space_before_synchronized_parentheses = true
ij_java_space_before_try_left_brace = true
ij_java_space_before_try_parentheses = true
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = true
ij_java_space_before_while_left_brace = true
ij_java_space_before_while_parentheses = true
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = true
ij_java_spaces_around_assignment_operators = true
ij_java_spaces_around_bitwise_operators = true
ij_java_spaces_around_equality_operators = true
ij_java_spaces_around_lambda_arrow = true
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = true
ij_java_spaces_around_relational_operators = true
ij_java_spaces_around_shift_operators = true
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = true
ij_java_ternary_operation_wrap = normal
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = normal
ij_java_throws_list_wrap = normal
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = normal
ij_java_visibility = public
ij_java_while_brace_force = always
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = false
ij_java_wrap_long_lines = true
[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false
[.editorconfig]
ij_editorconfig_align_group_field_declarations = false
ij_editorconfig_space_after_colon = false
ij_editorconfig_space_after_comma = true
ij_editorconfig_space_before_colon = false
ij_editorconfig_space_before_comma = false
ij_editorconfig_spaces_around_assignment_operators = true
[{*.ant, *.fxml, *.jhm, *.jnlp, *.jrxml, *.jspx, *.pom, *.rng, *.tagx, *.tld, *.wsdl, *.xml, *.xsd, *.xsl, *.xslt, *.xul}]
ij_xml_align_attributes = true
ij_xml_align_text = false
ij_xml_attribute_wrap = normal
ij_xml_block_comment_at_first_column = true
ij_xml_keep_blank_lines = 2
ij_xml_keep_indents_on_empty_lines = false
ij_xml_keep_line_breaks = true
ij_xml_keep_line_breaks_in_text = true
ij_xml_keep_whitespaces = false
ij_xml_keep_whitespaces_around_cdata = preserve
ij_xml_keep_whitespaces_inside_cdata = false
ij_xml_line_comment_at_first_column = true
ij_xml_space_after_tag_name = false
ij_xml_space_around_equals_in_attribute = false
ij_xml_space_inside_empty_tag = false
ij_xml_text_wrap = normal
[{*.bash, *.sh, *.zsh}]
indent_size = 2
tab_width = 2
ij_shell_binary_ops_start_line = false
ij_shell_keep_column_alignment_padding = false
ij_shell_minify_program = false
ij_shell_redirect_followed_by_space = false
ij_shell_switch_cases_indented = false
[{*.gant, *.gradle, *.groovy, *.gy}]
ij_groovy_align_group_field_declarations = false
ij_groovy_align_multiline_array_initializer_expression = false
ij_groovy_align_multiline_assignment = false
ij_groovy_align_multiline_binary_operation = false
ij_groovy_align_multiline_chained_methods = false
ij_groovy_align_multiline_extends_list = false
ij_groovy_align_multiline_for = true
ij_groovy_align_multiline_list_or_map = true
ij_groovy_align_multiline_method_parentheses = false
ij_groovy_align_multiline_parameters = true
ij_groovy_align_multiline_parameters_in_calls = false
ij_groovy_align_multiline_resources = true
ij_groovy_align_multiline_ternary_operation = false
ij_groovy_align_multiline_throws_list = false
ij_groovy_align_named_args_in_map = true
ij_groovy_align_throws_keyword = false
ij_groovy_array_initializer_new_line_after_left_brace = false
ij_groovy_array_initializer_right_brace_on_new_line = false
ij_groovy_array_initializer_wrap = off
ij_groovy_assert_statement_wrap = off
ij_groovy_assignment_wrap = off
ij_groovy_binary_operation_wrap = off
ij_groovy_blank_lines_after_class_header = 0
ij_groovy_blank_lines_after_imports = 1
ij_groovy_blank_lines_after_package = 1
ij_groovy_blank_lines_around_class = 1
ij_groovy_blank_lines_around_field = 0
ij_groovy_blank_lines_around_field_in_interface = 0
ij_groovy_blank_lines_around_method = 1
ij_groovy_blank_lines_around_method_in_interface = 1
ij_groovy_blank_lines_before_imports = 1
ij_groovy_blank_lines_before_method_body = 0
ij_groovy_blank_lines_before_package = 0
ij_groovy_block_brace_style = end_of_line
ij_groovy_block_comment_at_first_column = true
ij_groovy_call_parameters_new_line_after_left_paren = false
ij_groovy_call_parameters_right_paren_on_new_line = false
ij_groovy_call_parameters_wrap = off
ij_groovy_catch_on_new_line = false
ij_groovy_class_annotation_wrap = split_into_lines
ij_groovy_class_brace_style = end_of_line
ij_groovy_class_count_to_use_import_on_demand = 5
ij_groovy_do_while_brace_force = never
ij_groovy_else_on_new_line = false
ij_groovy_enum_constants_wrap = off
ij_groovy_extends_keyword_wrap = off
ij_groovy_extends_list_wrap = off
ij_groovy_field_annotation_wrap = split_into_lines
ij_groovy_finally_on_new_line = false
ij_groovy_for_brace_force = never
ij_groovy_for_statement_new_line_after_left_paren = false
ij_groovy_for_statement_right_paren_on_new_line = false
ij_groovy_for_statement_wrap = off
ij_groovy_if_brace_force = never
ij_groovy_import_annotation_wrap = 2
ij_groovy_indent_case_from_switch = true
ij_groovy_indent_label_blocks = true
ij_groovy_insert_inner_class_imports = false
ij_groovy_keep_blank_lines_before_right_brace = 2
ij_groovy_keep_blank_lines_in_code = 2
ij_groovy_keep_blank_lines_in_declarations = 2
ij_groovy_keep_control_statement_in_one_line = true
ij_groovy_keep_first_column_comment = true
ij_groovy_keep_indents_on_empty_lines = false
ij_groovy_keep_line_breaks = true
ij_groovy_keep_multiple_expressions_in_one_line = false
ij_groovy_keep_simple_blocks_in_one_line = false
ij_groovy_keep_simple_classes_in_one_line = true
ij_groovy_keep_simple_lambdas_in_one_line = true
ij_groovy_keep_simple_methods_in_one_line = true
ij_groovy_label_indent_absolute = false
ij_groovy_label_indent_size = 0
ij_groovy_lambda_brace_style = end_of_line
ij_groovy_layout_static_imports_separately = true
ij_groovy_line_comment_add_space = false
ij_groovy_line_comment_at_first_column = true
ij_groovy_method_annotation_wrap = split_into_lines
ij_groovy_method_brace_style = end_of_line
ij_groovy_method_call_chain_wrap = off
ij_groovy_method_parameters_new_line_after_left_paren = false
ij_groovy_method_parameters_right_paren_on_new_line = false
ij_groovy_method_parameters_wrap = off
ij_groovy_modifier_list_wrap = false
ij_groovy_names_count_to_use_import_on_demand = 3
ij_groovy_parameter_annotation_wrap = off
ij_groovy_parentheses_expression_new_line_after_left_paren = false
ij_groovy_parentheses_expression_right_paren_on_new_line = false
ij_groovy_prefer_parameters_wrap = false
ij_groovy_resource_list_new_line_after_left_paren = false
ij_groovy_resource_list_right_paren_on_new_line = false
ij_groovy_resource_list_wrap = off
ij_groovy_space_after_assert_separator = true
ij_groovy_space_after_colon = true
ij_groovy_space_after_comma = true
ij_groovy_space_after_comma_in_type_arguments = true
ij_groovy_space_after_for_semicolon = true
ij_groovy_space_after_quest = true
ij_groovy_space_after_type_cast = true
ij_groovy_space_before_annotation_parameter_list = false
ij_groovy_space_before_array_initializer_left_brace = false
ij_groovy_space_before_assert_separator = false
ij_groovy_space_before_catch_keyword = true
ij_groovy_space_before_catch_left_brace = true
ij_groovy_space_before_catch_parentheses = true
ij_groovy_space_before_class_left_brace = true
ij_groovy_space_before_closure_left_brace = true
ij_groovy_space_before_colon = true
ij_groovy_space_before_comma = false
ij_groovy_space_before_do_left_brace = true
ij_groovy_space_before_else_keyword = true
ij_groovy_space_before_else_left_brace = true
ij_groovy_space_before_finally_keyword = true
ij_groovy_space_before_finally_left_brace = true
ij_groovy_space_before_for_left_brace = true
ij_groovy_space_before_for_parentheses = true
ij_groovy_space_before_for_semicolon = false
ij_groovy_space_before_if_left_brace = true
ij_groovy_space_before_if_parentheses = true
ij_groovy_space_before_method_call_parentheses = false
ij_groovy_space_before_method_left_brace = true
ij_groovy_space_before_method_parentheses = false
ij_groovy_space_before_quest = true
ij_groovy_space_before_switch_left_brace = true
ij_groovy_space_before_switch_parentheses = true
ij_groovy_space_before_synchronized_left_brace = true
ij_groovy_space_before_synchronized_parentheses = true
ij_groovy_space_before_try_left_brace = true
ij_groovy_space_before_try_parentheses = true
ij_groovy_space_before_while_keyword = true
ij_groovy_space_before_while_left_brace = true
ij_groovy_space_before_while_parentheses = true
ij_groovy_space_in_named_argument = true
ij_groovy_space_in_named_argument_before_colon = false
ij_groovy_space_within_empty_array_initializer_braces = false
ij_groovy_space_within_empty_method_call_parentheses = false
ij_groovy_spaces_around_additive_operators = true
ij_groovy_spaces_around_assignment_operators = true
ij_groovy_spaces_around_bitwise_operators = true
ij_groovy_spaces_around_equality_operators = true
ij_groovy_spaces_around_lambda_arrow = true
ij_groovy_spaces_around_logical_operators = true
ij_groovy_spaces_around_multiplicative_operators = true
ij_groovy_spaces_around_regex_operators = true
ij_groovy_spaces_around_relational_operators = true
ij_groovy_spaces_around_shift_operators = true
ij_groovy_spaces_within_annotation_parentheses = false
ij_groovy_spaces_within_array_initializer_braces = false
ij_groovy_spaces_within_braces = true
ij_groovy_spaces_within_brackets = false
ij_groovy_spaces_within_cast_parentheses = false
ij_groovy_spaces_within_catch_parentheses = false
ij_groovy_spaces_within_for_parentheses = false
ij_groovy_spaces_within_gstring_injection_braces = false
ij_groovy_spaces_within_if_parentheses = false
ij_groovy_spaces_within_list_or_map = false
ij_groovy_spaces_within_method_call_parentheses = false
ij_groovy_spaces_within_method_parentheses = false
ij_groovy_spaces_within_parentheses = false
ij_groovy_spaces_within_switch_parentheses = false
ij_groovy_spaces_within_synchronized_parentheses = false
ij_groovy_spaces_within_try_parentheses = false
ij_groovy_spaces_within_tuple_expression = false
ij_groovy_spaces_within_while_parentheses = false
ij_groovy_special_else_if_treatment = true
ij_groovy_ternary_operation_wrap = off
ij_groovy_throws_keyword_wrap = off
ij_groovy_throws_list_wrap = off
ij_groovy_use_flying_geese_braces = false
ij_groovy_use_fq_class_names = false
ij_groovy_use_fq_class_names_in_javadoc = true
ij_groovy_use_relative_indents = false
ij_groovy_use_single_class_imports = true
ij_groovy_variable_annotation_wrap = off
ij_groovy_while_brace_force = never
ij_groovy_while_on_new_line = false
ij_groovy_wrap_long_lines = false
[{*.har, *.json}]
indent_size = 2
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = true
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{*.htm, *.html, *.sht, *.shtm, *.shtml}]
ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
ij_html_align_attributes = true
ij_html_align_text = false
ij_html_attribute_wrap = normal
ij_html_block_comment_at_first_column = true
ij_html_do_not_align_children_of_min_lines = 0
ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p
ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot
ij_html_enforce_quotes = false
ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var
ij_html_keep_blank_lines = 2
ij_html_keep_indents_on_empty_lines = false
ij_html_keep_line_breaks = true
ij_html_keep_line_breaks_in_text = true
ij_html_keep_whitespaces = false
ij_html_keep_whitespaces_inside = span, pre, textarea
ij_html_line_comment_at_first_column = true
ij_html_new_line_after_last_attribute = never
ij_html_new_line_before_first_attribute = never
ij_html_quote_style = double
ij_html_remove_new_line_before_tags = br
ij_html_space_after_tag_name = false
ij_html_space_around_equality_in_attribute = false
ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal
[{*.yaml, *.yml}]
indent_size = 2
ij_yaml_keep_indents_on_empty_lines = false
ij_yaml_keep_line_breaks = true
[*.md]
indent_size = 2

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
*.js linguist-language=Java
*.css linguist-language=Java
*.ftl linguist-language=FreeMarker
*.html linguist-language=Vue

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ["https://afdian.com/a/halo-dev"]

View File

@ -0,0 +1,80 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
id: preface
attributes:
value: |
Thank you for taking the time to fill out this issue report! Before we begin, we highly recommend reading through the [Open Source Guides](https://opensource.guide/), which will greatly improve our mutual efficiency.
**Before submitting, please check:**
- You have searched for related issues in the [issues](https://github.com/halo-dev/halo/issues) list.
- This is an issue with the Halo project itself. If it is not an issue with the project itself(For example: Installation and deployment issues.), it is recommended to submit it in the [Discussions](https://github.com/halo-dev/halo/discussions).
- You have tried disabling all plugins to rule out plugins as the cause of the problem.
- If it is an issue with plugins and themes, please submit it in the respective plugin and theme repositories.
- type: markdown
id: environment
attributes:
value: "## Environment"
- type: textarea
id: system-information
attributes:
label: "System information"
description: "Access the actuator page of the Console, click the copy button in the upper right corner, and paste the information here."
placeholder: |
- External url: https://demo.halo.run
- Start time: 2024-07-21 14:50
- Version: 2.x.x
- Build time: 2024-07-15 18:19
- Git Commit: 6d4bedd
- Java: IBM Semeru Runtime Open Edition / ...
- Database: PostgreSQL / 16.3 ...
- Operating system: Linux / 5.15.0-88 ...
- Activated theme: ...
- Enabled plugins:
- ...
validations:
required: true
- type: dropdown
id: operation-method
validations:
required: true
attributes:
label: "What is the project operation method?"
options:
- Docker
- Docker Compose
- Fat Jar
- Source Code
- type: markdown
id: details
attributes:
value: "## Details"
- type: textarea
id: what-happened
attributes:
label: "What happened?"
description: "For ease of management, please do not report multiple unrelated issues under the same issue."
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: "Reproduce Steps"
description: "If it can be consistently reproduced, please provide detailed steps."
placeholder: |
1. Open '...'
2. Click '...'
- type: textarea
id: logs
attributes:
label: "Relevant log output"
description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks."
render: shell
- type: textarea
id: additional-information
attributes:
label: "Additional information"
description: "If you have other information to note, you can fill it in here (screenshots, videos, etc.)."

View File

@ -0,0 +1,80 @@
name: Bug 反馈
description: 提交 Bug 反馈
labels: [bug]
body:
- type: markdown
id: preface
attributes:
value: |
感谢你花时间填写此错误报告!在开始之前,我们非常推荐阅读一遍[《开源软件指南》](https://opensource.guide/zh-hans/),这会在很大程度上提高我们彼此的效率。
**在提交之前,请检查:**
- 已经在 [issues](https://github.com/halo-dev/halo/issues) 列表中搜索了相关问题。
- 这是 Halo 项目本身存在的问题,如果是非项目本身的问题(如:安装部署问题),建议在 [Discussions](https://github.com/halo-dev/halo/discussions) 提交。
- 已经尝试过停用所有的插件,排除是插件导致的问题。
- 如果是插件和主题的问题,请在对应的插件和主题仓库提交。
- type: markdown
id: environment
attributes:
value: "## 环境信息"
- type: textarea
id: system-information
attributes:
label: "系统信息"
description: "访问 Console 的概览页面,点击右上角的复制按钮,将信息粘贴到此处。"
placeholder: |
- 外部访问地址: https://demo.halo.run
- 启动时间: 2024-07-21 14:50
- 版本: 2.x.x
- 构建时间: 2024-07-15 18:19
- Git Commit: 6d4bedd
- Java: IBM Semeru Runtime Open Edition / ...
- 数据库: PostgreSQL / 16.3 ...
- 操作系统: Linux / 5.15.0-88 ...
- 已激活主题: ...
- 已启动插件:
- ...
validations:
required: true
- type: dropdown
id: operation-method
validations:
required: true
attributes:
label: "使用的哪种方式运行?"
options:
- Docker
- Docker Compose
- Fat Jar
- Source Code
- type: markdown
id: details
attributes:
value: "## 详细信息"
- type: textarea
id: what-happened
attributes:
label: "发生了什么?"
description: "为了方便我们管理,请不要在同一个 issue 下报告多个不相关的问题。"
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: "复现步骤"
description: "如果可以稳定复现,请提供详细的步骤。"
placeholder: |
1. 打开 '...'
2. 点击 '...'
- type: textarea
id: logs
attributes:
label: "相关日志输出"
description: "请复制并粘贴任何相关的日志输出。这将自动格式化为代码,因此无需反引号。"
render: shell
- type: textarea
id: additional-information
attributes:
label: "附加信息"
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 对 Halo 有其他问题
url: https://bbs.halo.run
about: 如果你对 Halo 有其他想要提问的,我们欢迎到我们的官方社区进行提问。

View File

@ -0,0 +1,38 @@
name: Feature Request
description: File a feature request
body:
- type: markdown
id: preface
attributes:
value: |
Hello! Thank you for submitting a new feature suggestion for Halo. Before we begin, we highly recommend reading through the [Open Source Guides](https://opensource.guide/), which will greatly improve our mutual efficiency.
**Before submitting, please check:**
- You have searched for related issues in the [issues](https://github.com/halo-dev/halo/issues) list.
- This is a feature related to Halo. If it is not an issue with the project itself, it is recommended to submit it in the [Discussions](https://github.com/halo-dev/halo/discussions).
- If it is a feature suggestion for plugins and themes, please submit it in the respective plugin and theme repositories.
- type: markdown
id: environment
attributes:
value: "## Environment"
- type: input
id: version
attributes:
label: "Your current Halo version"
- type: markdown
id: details
attributes:
value: "## Details"
- type: textarea
id: description
attributes:
label: "Describe this feature"
description: "For ease of management, please do not submit multiple unrelated features under the same issue."
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: "Additional information"
description: "If you have other information to note, you can fill it in here (screenshots, videos, etc.)."

View File

@ -0,0 +1,38 @@
name: 新特性建议
description: 提交新特性建议
body:
- type: markdown
id: preface
attributes:
value: |
你好!感谢你为 Halo 提交新特性建议。在开始之前,我们非常推荐阅读一遍[《开源软件指南》](https://opensource.guide/zh-hans/),这会在很大程度上提高我们彼此的效率。
**在提交之前,请检查:**
- 已经在 [issues](https://github.com/halo-dev/halo/issues) 列表中搜索了相关问题。
- 这是和 Halo 相关的特性,如果是非项目本身的问题,建议在 [Discussions](https://github.com/halo-dev/halo/discussions) 提交。
- 如果是插件和主题特性建议,请在对应的插件和主题仓库提交。
- type: markdown
id: environment
attributes:
value: "## 环境信息"
- type: input
id: version
attributes:
label: "你当前使用的版本"
- type: markdown
id: details
attributes:
value: "## 详细信息"
- type: textarea
id: description
attributes:
label: "描述一下此特性"
description: "为了方便我们管理,请不要在同一个 issue 下提交多个没有相关性的特性。"
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: "附加信息"
description: "如果你还有其他需要提供的信息,可以在这里填写(可以提供截图、视频等)。"

View File

@ -0,0 +1,75 @@
name: "Docker buildx and push"
description: "Buildx and push the Docker image."
inputs:
ghcr-token:
description: Token of current GitHub account in GitHub container registry.
required: false
default: ""
dockerhub-user:
description: "User name for the DockerHub account"
required: false
default: ""
dockerhub-token:
description: Token for the DockerHub account
required: false
default: ""
push:
description: Should push the docker image or not.
required: false
default: "false"
platforms:
description: Target platforms for building image
required: false
default: "linux/amd64,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x"
image-name:
description: The basic name of docker.
required: false
default: "halo"
runs:
using: "composite"
steps:
- name: Docker meta for Halo
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}
halohub/${{ inputs.image-name }}
tags: |
type=schedule,pattern=nightly-{{date 'YYYYMMDD'}},enabled=${{ github.event_name == 'schedule' }}
type=ref,event=branch,enabled=${{ github.event_name == 'push' }}
type=ref,event=pr,enabled=${{ github.event_name == 'pull_request' }}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{ version }}
type=sha,enabled=${{ github.event_name == 'push' }}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
if: inputs.ghcr-token != '' && github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ inputs.ghcr-token }}
- name: Login to DockerHub
if: inputs.dockerhub-token != '' && github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ inputs.dockerhub-user }}
password: ${{ inputs.dockerhub-token }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: ${{ inputs.platforms }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
push: ${{ (inputs.ghcr-token != '' || inputs.dockerhub-token != '') && inputs.push == 'true' }}

40
.github/actions/setup-env/action.yaml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Setup Environment
description: Setup environment to check and build Halo, including console and core projects.
inputs:
node-version:
description: Node.js version.
required: false
default: "20"
pnpm-version:
description: pnpm version.
required: false
default: "9"
java-version:
description: Java version.
required: false
default: "17"
runs:
using: "composite"
steps:
- uses: pnpm/action-setup@v3
name: Setup pnpm
with:
version: ${{ inputs.pnpm-version }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "pnpm"
cache-dependency-path: "ui/pnpm-lock.yaml"
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: "temurin"
cache: "gradle"
java-version: ${{ inputs.java-version }}

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

59
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,59 @@
<!-- Thanks for sending a pull request! Here are some tips for you:
1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md>
1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md>.
2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.
3. 请确保你已经添加并运行了适当的测试。
3. Ensure you have added or ran the appropriate tests for your PR.
-->
#### What type of PR is this?
<!--
添加其中一个类别:
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind improvement
适当添加其中一个或多个类别(可选):
Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
#### What this PR does / why we need it:
#### Which issue(s) this PR fixes:
<!--
PR 合并时自动关闭 issue。
Automatically closes linked issue when PR is merged.
用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)`
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #
#### Special notes for your reviewer:
#### Does this PR introduce a user-facing change?
<!--
如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`
否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新Break Change
Release Note 需要以 `action required` 开头。
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
```

126
.github/workflows/halo.yaml vendored Normal file
View File

@ -0,0 +1,126 @@
name: Halo Workflow
on:
pull_request:
branches:
- main
- release-*
paths:
- "**"
- "!**.md"
push:
branches:
- main
- release-*
paths:
- "**"
- "!**.md"
release:
types:
- published
concurrency:
group: ${{github.workflow}} - ${{github.ref}}
cancel-in-progress: true
jobs:
test:
if: github.event_name == 'pull_request' || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Environment
uses: ./.github/actions/setup-env
- name: Check Halo
run: ./gradlew check
- name: Upload coverage reports to Codecov
if: github.repository == 'halo-dev/halo'
uses: codecov/codecov-action@v4
build:
runs-on: ubuntu-latest
if: always() && (needs.test.result == 'skipped' || needs.test.result == 'success')
needs: test
steps:
- uses: actions/checkout@v4
- name: Setup Environment
uses: ./.github/actions/setup-env
- name: Reset version of Halo
if: github.event_name == 'release'
shell: bash
run: |
# Set the version with tag name when releasing
version=${{ github.event.release.tag_name }}
version=${version#v}
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
- name: Build Halo
run: ./gradlew clean && ./gradlew downloadPluginPresets && ./gradlew build -x check
- name: Upload Artifacts
if: github.repository == 'halo-dev/halo'
uses: actions/upload-artifact@v4
with:
name: halo-artifacts
path: application/build/libs
retention-days: 1
github-release:
runs-on: ubuntu-latest
if: always() && needs.build.result == 'success' && github.event_name == 'release'
needs: build
steps:
- uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: halo-artifacts
path: application/build/libs
- name: Upload Artifacts
if: github.repository == 'halo-dev/halo'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.event.release.tag_name }} application/build/libs/*
docker-build-and-push:
if: always() && needs.build.result == 'success' && (github.event_name == 'push' || github.event_name == 'release')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: halo-artifacts
path: application/build/libs
- name: Docker Buildx and Push
uses: ./.github/actions/docker-buildx-push
with:
image-name: ${{ github.event_name == 'release' && 'halo' || 'halo-dev' }}
ghcr-token: ${{ secrets.GITHUB_TOKEN }}
dockerhub-user: ${{ secrets.DOCKER_USERNAME }}
dockerhub-token: ${{ secrets.DOCKER_TOKEN }}
push: true
platforms: linux/amd64,linux/arm64/v8,linux/ppc64le,linux/s390x
e2e-test:
if: always() && needs.build.result == 'success' && (github.event_name == 'pull_request' || github.event_name == 'push')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: halo-artifacts
path: application/build/libs
- name: Docker Build
uses: docker/build-push-action@v5
with:
tags: ghcr.io/halo-dev/halo-dev:main
push: false
context: .
- name: E2E Testing
continue-on-error: true
run: |
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod u+x /usr/local/bin/docker-compose
cd e2e && make all

78
.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
### Maven
target/
logs/
!.mvn/wrapper/maven-wrapper.jar
### Gradle
.gradle
build/
out/
!gradle/wrapper/gradle-wrapper.jar
bin/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
log/
### NetBeans ###
nbproject/private/
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
### Mac
.DS_Store
*/.DS_Store
### VS Code ###
*.project
*.factorypath
### Compiled class file
*.class
### Log file
*.log
### BlueJ files
*.ctxt
### Mobile Tools for Java (J2ME)
.mtj.tmp/
### Package Files
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
### VSCode
.vscode
### Local file
application-local.yml
application-local.yaml
application-local.properties
### Zip file for test
!application/src/test/resources/themes/*.zip
!application/src/main/resources/themes/*.zip
application/src/main/resources/console/
application/src/main/resources/uc/
application/src/main/resources/presets/

8
.gitpod.yml Normal file
View File

@ -0,0 +1,8 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
# and commit this file to your remote git repository to share the goodness with others.
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
tasks:
- init: ./gradlew clean build -x check && sdk install java 17.0.3-ms

128
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hi@halo.run.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

68
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,68 @@
> 欢迎你参与 Halo 的开发,下面是参与代码贡献的指南,以供参考。
### 代码贡献步骤
#### 0. 提交 issue
任何新功能或者功能改进建议都先提交 issue 讨论一下再进行开发bug 修复可以直接提交 pull request。
#### 1. Fork 此仓库
点击右上角的 `fork` 按钮即可。
#### 2. Clone 仓库到本地
```bash
git clone https://github.com/{YOUR_USERNAME}/halo
git submodule init
git submodule update
```
#### 3. 创建新的开发分支
```bash
git checkout -b {BRANCH_NAME}
```
#### 4. 提交代码
```bash
git push origin {BRANCH_NAME}
```
#### 5. 提交 pull request
回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `main` 分支。
然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。
#### 6. 更新主仓库代码到自己的仓库
```bash
git remote add upstream git@github.com:halo-dev/halo.git
git pull upstream main
git push
```
### E2E
Please consider adding some [e2e test cases](e2e/README.md) to make sure the APIs work as expected.
### 开发规范
请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style),请确保所有代码格式化之后再提交。
### Usage of Cherry Pick Script
We can use the cherry pick script to cherry-pick commits in pull request as follows:
```bash
GITHUB_USER={your_github_user} hack/cherry_pick_pull.sh upstream/{target_branch} {pull_request_number}
```
> This script is from <https://github.com/kubernetes/kubernetes/blob/master/hack/cherry_pick_pull.sh>.

28
Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM eclipse-temurin:21-jre as builder
WORKDIR application
ARG JAR_FILE=application/build/libs/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
################################
FROM ibm-semeru-runtimes:open-21-jre
LABEL maintainer="johnniang <johnniang@foxmail.com>"
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENV JVM_OPTS="-Xmx256m -Xms256m" \
HALO_WORK_DIR="/root/.halo2" \
SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/root/.halo2/" \
TZ=Asia/Shanghai
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone
Expose 8090
ENTRYPOINT ["sh", "-c", "java ${JVM_OPTS} org.springframework.boot.loader.launch.JarLauncher ${0} ${@}"]

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

11
OWNERS Normal file
View File

@ -0,0 +1,11 @@
reviewers:
- ruibaby
- guqing
- JohnNiang
- wan92hen
- LIlGG
approvers:
- ruibaby
- guqing
- JohnNiang

71
README.md Normal file
View File

@ -0,0 +1,71 @@
<p align="center">
<a href="https://www.halo.run" target="_blank" rel="noopener noreferrer">
<img width="100" src="https://www.halo.run/logo" alt="Halo logo" />
</a>
</p>
<p align="center"><b>Halo</b> [ˈheɪloʊ],强大易用的开源建站工具。</p>
<p align="center">
<a href="https://github.com/halo-dev/halo/releases"><img alt="GitHub release" src="https://img.shields.io/github/release/halo-dev/halo.svg?style=flat-square&include_prereleases" /></a>
<a href="https://hub.docker.com/r/halohub/halo"><img alt="Docker pulls" src="https://img.shields.io/docker/pulls/halohub/halo?style=flat-square" /></a>
<a href="https://github.com/halo-dev/halo/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/halo-dev/halo.svg?style=flat-square" /></a>
<a href="https://github.com/halo-dev/halo/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/halo-dev/halo/halo.yaml?branch=main&style=flat-square" /></a>
<a href="https://codecov.io/gh/halo-dev/halo"><img alt="Codecov percentage" src="https://img.shields.io/codecov/c/github/halo-dev/halo/main?style=flat-square&token=YsRUg9fall"/></a>
<a href="https://www.producthunt.com/posts/halo-6b401e75-bb58-4dff-9fe9-2ada3323c874?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-halo&#0045;6b401e75&#0045;bb58&#0045;4dff&#0045;9fe9&#0045;2ada3323c874" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=407442&theme=light" alt="Halo - Powerful&#0032;and&#0032;easy&#0045;to&#0045;use&#0032;Open&#0045;Source&#0032;website&#0032;building&#0032;tool | Product Hunt" style="height: 20px;" height="20px" /></a>
<br />
<a href="https://www.halo.run">官网</a>
<a href="https://docs.halo.run">文档</a>
<a href="https://bbs.halo.run">社区</a>
<a href="https://gitee.com/halo-dev">Gitee</a>
<a href="https://t.me/halo_dev">Telegram 频道</a>
</p>
[![Watch the video](https://www.halo.run/upload/halo-github-screenshot.png)](https://www.bilibili.com/video/BV15x4y1U7RU/?share_source=copy_web&vd_source=0ab6cf86ca512a363f04f18b86f55b86)
------------------------------
## 快速开始
```bash
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.18
```
以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose>
## 在线体验
- 环境地址:<https://demo.halo.run>
- 后台地址:<https://demo.halo.run/console>
- 用户名:`demo`
- 密码:`P@ssw0rd123..`
## 生态
可访问 [官方应用市场](https://www.halo.run/store/apps) 或 [awesome-halo 仓库](https://github.com/halo-sigs/awesome-halo) 查看适用于 Halo 2.x 的主题和插件。
## 许可证
[![license](https://img.shields.io/github/license/halo-dev/halo.svg?style=flat-square)](https://github.com/halo-dev/halo/blob/master/LICENSE)
Halo 使用 GPL-v3.0 协议开源,请遵守开源协议。
## 赞助
如果 Halo 对你有帮助,欢迎[赞助我们](https://afdian.com/a/halo-dev),感谢以下赞助者对 Halo 项目的支持:
<p align="center">
<a target="_blank" href="https://afdian.com/a/halo-dev">
<img alt="sponsors" src="https://github.com/halo-sigs/sponsor-images/blob/main/sponsorkit/sponsors.svg?raw=true">
</a>
</p>
## 贡献
参考 [CONTRIBUTING](https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md)。
<a href="https://github.com/halo-dev/halo/graphs/contributors"><img src="https://opencollective.com/halo/contributors.svg?width=890&button=false" /></a>
## 状态
![Repobeats analytics](https://repobeats.axiom.co/api/embed/ad008b2151c22e7cf734d2688befaa795d593b95.svg "Repobeats analytics image")

42
SECURITY.md Normal file
View File

@ -0,0 +1,42 @@
# Security Policy
## Supported Versions
Halo currently supports the versions listed below, where as:
- :white_check_mark: indicates an active development roadmap, is therefore maintaining, and **will** receive Security
Vulnerability Report.
- :x: indicates such version has already deprecated and **will not** be receiving Security Vulnerability Report.
| Version | Supported |
| ------- | ------------------ |
| 0.x | :x: |
| 1.x | :x: |
| 2.x | :white_check_mark: |
## Reporting a Vulnerability
We first appreciate and are very thankful that you've found a vulnerability issue in Halo! By disclosing such issue to
Halo development team you are helping Halo to become a much more safer project than before! ;)
To protect the existing users of Halo, we kindly ask you to not disclose the vulnerability to anyone except the Halo
development team before a fix has been rolled out.
To Report a Vulnerability, please complete the form below, and send such report by email to `hi@halo.run`.
```
Vulnerability has been observed in...
- Docker? [n/y]:
if yes for the question above,
- `docker -v`:
- `docker images halohub/halo`:
- by `java -jar halo.jar`? [n/y]:
if yes for the question above,
- `uname -a`:
- `java -version`:
- Affected by Halo version(s) [e.g. v2.4.0]:
- Vulnerability self-scoring [1-10]:
- Would you like to be attributed? (Whether you agree us to appreciate you by putting your name in the CHANGELOG of the next fix release) [n/y]:
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

101
api/build.gradle Normal file
View File

@ -0,0 +1,101 @@
plugins {
id 'java-library'
id 'halo.publish'
id 'jacoco'
id "io.freefair.lombok"
}
group = 'run.halo.app'
description = 'API of halo project, connecting by other projects.'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"
javadoc.options.encoding = "UTF-8"
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
}
dependencies {
api platform(project(':platform:application'))
api 'org.springframework.boot:spring-boot-starter-actuator'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
api 'org.springframework.boot:spring-boot-starter-webflux'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-r2dbc'
api 'org.springframework.session:spring-session-core'
// Spring Security
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.security:spring-security-oauth2-jose'
api 'org.springframework.security:spring-security-oauth2-client'
api 'org.springframework.security:spring-security-oauth2-resource-server'
// Cache
api "org.springframework.boot:spring-boot-starter-cache"
api "com.github.ben-manes.caffeine:caffeine"
api "org.springdoc:springdoc-openapi-starter-webflux-ui"
api 'org.openapi4j:openapi-schema-validator'
api "net.bytebuddy:byte-buddy"
// Apache Lucene
api "org.apache.lucene:lucene-core"
api "org.apache.lucene:lucene-queryparser"
api "org.apache.lucene:lucene-highlighter"
api "org.apache.lucene:lucene-backward-codecs"
api 'org.apache.lucene:lucene-analysis-common'
api "org.apache.commons:commons-lang3"
api "io.seruco.encoding:base62"
api "org.pf4j:pf4j"
api "com.google.guava:guava"
api "org.jsoup:jsoup"
api "io.github.java-diff-utils:java-diff-utils"
api "org.springframework.integration:spring-integration-core"
api "com.github.java-json-tools:json-patch"
api "org.thymeleaf.extras:thymeleaf-extras-springsecurity6"
api 'org.apache.tika:tika-core'
api "io.github.resilience4j:resilience4j-spring-boot3"
api "io.github.resilience4j:resilience4j-reactor"
api "com.j256.two-factor-auth:two-factor-auth"
runtimeOnly 'io.r2dbc:r2dbc-h2'
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'org.postgresql:r2dbc-postgresql'
runtimeOnly 'org.mariadb:r2dbc-mariadb'
runtimeOnly 'io.asyncer:r2dbc-mysql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
}
java {
withSourcesJar()
withJavadocJar()
}
tasks.named('test') {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
tasks.named('jacocoTestReport') {
reports {
xml.required = true
html.required = false
}
}

View File

@ -0,0 +1,43 @@
package run.halo.app.content;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import run.halo.app.core.extension.content.Snapshot;
/**
* @author guqing
* @since 2.0.0
*/
@Data
@Builder
public class ContentWrapper {
private String snapshotName;
private String raw;
private String content;
private String rawType;
public static ContentWrapper patchSnapshot(Snapshot patchSnapshot, Snapshot baseSnapshot) {
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
String baseSnapshotName = baseSnapshot.getMetadata().getName();
if (StringUtils.equals(patchSnapshot.getMetadata().getName(), baseSnapshotName)) {
return ContentWrapper.builder()
.snapshotName(patchSnapshot.getMetadata().getName())
.raw(patchSnapshot.getSpec().getRawPatch())
.content(patchSnapshot.getSpec().getContentPatch())
.rawType(patchSnapshot.getSpec().getRawType())
.build();
}
String patchedContent = PatchUtils.applyPatch(baseSnapshot.getSpec().getContentPatch(),
patchSnapshot.getSpec().getContentPatch());
String patchedRaw = PatchUtils.applyPatch(baseSnapshot.getSpec().getRawPatch(),
patchSnapshot.getSpec().getRawPatch());
return ContentWrapper.builder()
.snapshotName(patchSnapshot.getMetadata().getName())
.raw(patchedRaw)
.content(patchedContent)
.rawType(patchSnapshot.getSpec().getRawType())
.build();
}
}

View File

@ -0,0 +1,32 @@
package run.halo.app.content;
import java.util.Set;
import lombok.Data;
import lombok.experimental.Accessors;
import org.pf4j.ExtensionPoint;
import reactor.core.publisher.Mono;
public interface ExcerptGenerator extends ExtensionPoint {
Mono<String> generate(ExcerptGenerator.Context context);
@Data
@Accessors(chain = true)
class Context {
private String raw;
/**
* html content.
*/
private String content;
private String rawType;
/**
* keywords in the content to help the excerpt generation more accurate.
*/
private Set<String> keywords;
/**
* Max length of the generated excerpt.
*/
private int maxLength;
}
}

View File

@ -0,0 +1,88 @@
package run.halo.app.content;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.difflib.DiffUtils;
import com.github.difflib.patch.AbstractDelta;
import com.github.difflib.patch.ChangeDelta;
import com.github.difflib.patch.Chunk;
import com.github.difflib.patch.DeleteDelta;
import com.github.difflib.patch.DeltaType;
import com.github.difflib.patch.InsertDelta;
import com.github.difflib.patch.Patch;
import com.github.difflib.patch.PatchFailedException;
import com.google.common.base.Splitter;
import java.util.Collections;
import java.util.List;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import run.halo.app.infra.utils.JsonUtils;
/**
* @author guqing
* @since 2.0.0
*/
public class PatchUtils {
private static final String DELIMITER = "\n";
private static final Splitter lineSplitter = Splitter.on(DELIMITER);
public static Patch<String> create(String deltasJson) {
List<Delta> deltas = JsonUtils.jsonToObject(deltasJson, new TypeReference<>() {
});
Patch<String> patch = new Patch<>();
for (Delta delta : deltas) {
StringChunk sourceChunk = delta.getSource();
StringChunk targetChunk = delta.getTarget();
Chunk<String> orgChunk = new Chunk<>(sourceChunk.getPosition(), sourceChunk.getLines(),
sourceChunk.getChangePosition());
Chunk<String> revChunk = new Chunk<>(targetChunk.getPosition(), targetChunk.getLines(),
targetChunk.getChangePosition());
switch (delta.getType()) {
case DELETE -> patch.addDelta(new DeleteDelta<>(orgChunk, revChunk));
case INSERT -> patch.addDelta(new InsertDelta<>(orgChunk, revChunk));
case CHANGE -> patch.addDelta(new ChangeDelta<>(orgChunk, revChunk));
default -> throw new IllegalArgumentException("Unsupported delta type.");
}
}
return patch;
}
public static String patchToJson(Patch<String> patch) {
List<AbstractDelta<String>> deltas = patch.getDeltas();
return JsonUtils.objectToJson(deltas);
}
public static String applyPatch(String original, String patchJson) {
Patch<String> patch = PatchUtils.create(patchJson);
try {
return String.join(DELIMITER, patch.applyTo(breakLine(original)));
} catch (PatchFailedException e) {
throw new RuntimeException(e);
}
}
public static String diffToJsonPatch(String original, String revised) {
Patch<String> patch = DiffUtils.diff(breakLine(original), breakLine(revised));
return PatchUtils.patchToJson(patch);
}
public static List<String> breakLine(String content) {
if (StringUtils.isBlank(content)) {
return Collections.emptyList();
}
return lineSplitter.splitToList(content);
}
@Data
public static class Delta {
private StringChunk source;
private StringChunk target;
private DeltaType type;
}
@Data
public static class StringChunk {
private int position;
private List<String> lines;
private List<Integer> changePosition;
}
}

View File

@ -0,0 +1,15 @@
package run.halo.app.content;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PostContentService {
Mono<ContentWrapper> getHeadContent(String postName);
Mono<ContentWrapper> getReleaseContent(String postName);
Mono<ContentWrapper> getSpecifiedContent(String postName, String snapshotName);
Flux<String> listSnapshots(String postName);
}

View File

@ -0,0 +1,26 @@
package run.halo.app.content.comment;
import org.pf4j.ExtensionPoint;
import reactor.core.publisher.Mono;
import run.halo.app.extension.Extension;
import run.halo.app.extension.Ref;
/**
* Comment subject.
*
* @author guqing
* @since 2.0.0
*/
public interface CommentSubject<T extends Extension> extends ExtensionPoint {
Mono<T> get(String name);
default Mono<SubjectDisplay> getSubjectDisplay(String name) {
return Mono.empty();
}
boolean supports(Ref ref);
record SubjectDisplay(String title, String url, String kindName) {
}
}

View File

@ -0,0 +1,34 @@
package run.halo.app.core.endpoint;
import org.springframework.web.reactive.socket.WebSocketHandler;
import run.halo.app.extension.GroupVersion;
/**
* Endpoint for WebSocket.
*
* @author johnniang
*/
public interface WebSocketEndpoint {
/**
* Path of the URL after group version.
*
* @return path of the URL.
*/
String urlPath();
/**
* Group and version parts of the endpoint.
*
* @return GroupVersion.
*/
GroupVersion groupVersion();
/**
* Real WebSocket handler for the endpoint.
*
* @return WebSocket handler.
*/
WebSocketHandler handler();
}

View File

@ -0,0 +1,36 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.AnnotationSetting.KIND;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupKind;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = KIND,
plural = "annotationsettings", singular = "annotationsetting")
public class AnnotationSetting extends AbstractExtension {
public static final String TARGET_REF_LABEL = "halo.run/target-ref";
public static final String KIND = "AnnotationSetting";
@Schema(requiredMode = REQUIRED)
private AnnotationSettingSpec spec;
@Data
public static class AnnotationSettingSpec {
@Schema(requiredMode = REQUIRED)
private GroupKind targetRef;
@Schema(requiredMode = REQUIRED, minLength = 1)
private List<Object> formSchema;
}
}

View File

@ -0,0 +1,80 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* Auth provider extension.
*
* @author guqing
* @since 2.4.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@GVK(group = "auth.halo.run", version = "v1alpha1", kind = "AuthProvider",
singular = "authprovider", plural = "authproviders")
public class AuthProvider extends AbstractExtension {
public static final String AUTH_BINDING_LABEL = "auth.halo.run/auth-binding";
public static final String PRIVILEGED_LABEL = "auth.halo.run/privileged";
@Schema(requiredMode = REQUIRED)
private AuthProviderSpec spec;
@Data
@ToString
public static class AuthProviderSpec {
@Schema(requiredMode = REQUIRED, description = "Display name of the auth provider")
private String displayName;
private String description;
private String logo;
private String website;
private String helpPage;
@Schema(requiredMode = REQUIRED, description = "Authentication url of the auth provider")
private String authenticationUrl;
private String bindingUrl;
private String unbindUrl;
private int priority;
@Schema(requiredMode = NOT_REQUIRED)
private SettingRef settingRef;
@Schema(requiredMode = NOT_REQUIRED)
private ConfigMapRef configMapRef;
}
@Data
@ToString
public static class SettingRef {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String group;
}
@Data
@ToString
public static class ConfigMapRef {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
}
}

View File

@ -0,0 +1,41 @@
package run.halo.app.core.extension;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Metadata;
/**
* A counter for number of requests by extension resource name.
*
* @author guqing
* @since 2.0.0
*/
@Data
@GVK(group = "metrics.halo.run", version = "v1alpha1", kind = "Counter", plural = "counters",
singular = "counter")
@EqualsAndHashCode(callSuper = true)
public class Counter extends AbstractExtension {
private Integer visit;
private Integer upvote;
private Integer downvote;
private Integer totalComment;
private Integer approvedComment;
public static Counter emptyCounter(String name) {
Counter counter = new Counter();
counter.setMetadata(new Metadata());
counter.getMetadata().setName(name);
counter.setUpvote(0);
counter.setTotalComment(0);
counter.setApprovedComment(0);
counter.setVisit(0);
return counter;
}
}

View File

@ -0,0 +1,65 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.lang.NonNull;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = Device.GROUP, version = Device.VERSION, kind = Device.KIND, plural = "devices",
singular = "device")
public class Device extends AbstractExtension {
public static final String GROUP = "security.halo.run";
public static final String VERSION = "v1alpha1";
public static final String KIND = "Device";
@Schema(requiredMode = REQUIRED)
private Spec spec;
@Getter(onMethod_ = @NonNull)
private Status status = new Status();
public void setStatus(Status status) {
this.status = (status == null ? new Status() : status);
}
@Data
@Accessors(chain = true)
@Schema(name = "DeviceSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String sessionId;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String principalName;
@Schema(requiredMode = REQUIRED, maxLength = 129)
private String ipAddress;
@Schema(maxLength = 500)
private String userAgent;
private String rememberMeSeriesId;
private Instant lastAccessedTime;
private Instant lastAuthenticatedTime;
}
@Data
@Accessors(chain = true)
@Schema(name = "DeviceStatus")
public static class Status {
private String browser;
private String os;
}
}

View File

@ -0,0 +1,39 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.LinkedHashSet;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = "Menu", plural = "menus", singular = "menu")
public class Menu extends AbstractExtension {
@Schema(description = "The spec of menu.", requiredMode = REQUIRED)
private Spec spec;
@Data
@Schema(name = "MenuSpec")
public static class Spec {
@Schema(description = "The display name of the menu.", requiredMode = REQUIRED)
private String displayName;
@Schema(description = "Names of menu children below this menu.")
@ArraySchema(
arraySchema = @Schema(description = "Menu items of this menu."),
schema = @Schema(description = "Name of menu item.")
)
private LinkedHashSet<String> menuItems;
}
}

View File

@ -0,0 +1,84 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.LinkedHashSet;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Ref;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = "MenuItem",
plural = "menuitems", singular = "menuitem")
public class MenuItem extends AbstractExtension {
@Schema(description = "The spec of menu item.", requiredMode = REQUIRED)
private MenuItemSpec spec;
@Schema(description = "The status of menu item.")
private MenuItemStatus status;
public enum Target {
BLANK("_blank"),
SELF("_self"),
PARENT("_parent"),
TOP("_top");
private final String value;
@JsonCreator
Target(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
}
@Data
public static class MenuItemSpec {
@Schema(description = "The display name of menu item.")
private String displayName;
@Schema(description = "The href of this menu item.")
private String href;
@Schema(description = "The <a> target attribute of this menu item.")
private Target target;
@Schema(description = "The priority is for ordering.")
private Integer priority;
@ArraySchema(
arraySchema = @Schema(description = "Children of this menu item"),
schema = @Schema(description = "The name of menu item child"))
private LinkedHashSet<String> children;
@Schema(description = "Target reference. Like Category, Tag, Post or SinglePage")
private Ref targetRef;
}
@Data
public static class MenuItemStatus {
@Schema(description = "Calculated Display name of menu item.")
private String displayName;
@Schema(description = "Calculated href of manu item.")
private String href;
}
}

View File

@ -0,0 +1,164 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.pf4j.PluginState;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.infra.ConditionList;
/**
* A custom resource for Plugin.
*
* @author guqing
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = "plugin.halo.run", version = "v1alpha1", kind = "Plugin", plural = "plugins",
singular = "plugin")
@EqualsAndHashCode(callSuper = true)
public class Plugin extends AbstractExtension {
@Schema(requiredMode = REQUIRED)
private PluginSpec spec;
private PluginStatus status;
/**
* Gets plugin status.
*
* @return empty object if status is null.
*/
@NonNull
@JsonIgnore
public PluginStatus statusNonNull() {
if (this.status == null) {
this.status = new PluginStatus();
}
return status;
}
@Data
public static class PluginSpec {
private String displayName;
/**
* plugin version.
*
* @see <a href="semver.org">semantic version</a>
*/
@Schema(requiredMode = REQUIRED,
pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
+ ".[0-9a-zA-Z-]+)*))?$")
private String version;
private PluginAuthor author;
private String logo;
private Map<String, String> pluginDependencies = new HashMap<>(4);
private String homepage;
private String repo;
private String issues;
private String description;
private List<License> license;
/**
* SemVer format.
*/
private String requires = "*";
@Deprecated
private String pluginClass;
private Boolean enabled = false;
private String settingName;
private String configMapName;
}
/**
* In the future, we may consider using {@link run.halo.app.infra.model.License} instead of it.
* But now, replace it will lead to incompatibility with downstream.
*/
@Data
public static class License {
private String name;
private String url;
}
@Data
public static class PluginStatus {
private Phase phase;
private ConditionList conditions;
private Instant lastStartTime;
private PluginState lastProbeState;
private String entry;
private String stylesheet;
private String logo;
@Schema(description = "Load location of the plugin, often a path.")
private URI loadLocation;
public static ConditionList nullSafeConditions(@NonNull PluginStatus status) {
Assert.notNull(status, "The status must not be null.");
if (status.getConditions() == null) {
status.setConditions(new ConditionList());
}
return status.getConditions();
}
}
public enum Phase {
PENDING,
STARTING,
CREATED,
DISABLING,
DISABLED,
RESOLVED,
STARTED,
STOPPED,
FAILED,
UNKNOWN,
;
}
@Data
@ToString
public static class PluginAuthor {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
private String website;
}
}

View File

@ -0,0 +1,37 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "security.halo.run", version = "v1alpha1", kind = "RememberMeToken", plural =
"remembermetokens", singular = "remembermetoken")
public class RememberMeToken extends AbstractExtension {
@Schema(requiredMode = REQUIRED)
private Spec spec;
@Data
@Accessors(chain = true)
@Schema(name = "RememberMeTokenSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String username;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String series;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String tokenValue;
private Instant lastUsed;
}
}

View File

@ -0,0 +1,29 @@
package run.halo.app.core.extension;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>The reverse proxy custom resource is used to configure a path to proxy it to a directory or
* file.</p>
* <p>HTTP proxy may be added in the future.</p>
*
* @author guqing
* @since 2.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "plugin.halo.run", kind = "ReverseProxy", version = "v1alpha1",
plural = "reverseproxies", singular = "reverseproxy")
public class ReverseProxy extends AbstractExtension {
private List<ReverseProxyRule> rules;
public record ReverseProxyRule(String path, FileReverseProxyProvider file) {
}
public record FileReverseProxyProvider(String directory, String filename) {
}
}

View File

@ -0,0 +1,187 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Arrays.compare;
import static run.halo.app.core.extension.Role.GROUP;
import static run.halo.app.core.extension.Role.KIND;
import static run.halo.app.core.extension.Role.VERSION;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* @author guqing
* @since 2.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@GVK(group = GROUP,
version = VERSION,
kind = KIND,
plural = "roles",
singular = "role")
public class Role extends AbstractExtension {
public static final String ROLE_DEPENDENCY_RULES =
"rbac.authorization.halo.run/dependency-rules";
public static final String ROLE_AGGREGATE_LABEL_PREFIX =
"rbac.authorization.halo.run/aggregate-to-";
public static final String ROLE_DEPENDENCIES_ANNO = "rbac.authorization.halo.run/dependencies";
public static final String UI_PERMISSIONS_ANNO = "rbac.authorization.halo.run/ui-permissions";
public static final String SYSTEM_RESERVED_LABELS =
"rbac.authorization.halo.run/system-reserved";
public static final String HIDDEN_LABEL_NAME = "halo.run/hidden";
public static final String TEMPLATE_LABEL_NAME = "halo.run/role-template";
public static final String UI_PERMISSIONS_AGGREGATED_ANNO =
"rbac.authorization.halo.run/ui-permissions-aggregated";
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "Role";
@Schema(requiredMode = REQUIRED)
List<PolicyRule> rules;
/**
* PolicyRule holds information that describes a policy rule, but does not contain information
* about whom the rule applies to or which namespace the rule applies to.
*
* @author guqing
* @since 2.0.0
*/
@Getter
@EqualsAndHashCode
public static class PolicyRule implements Comparable<PolicyRule> {
/**
* APIGroups is the name of the APIGroup that contains the resources.
* If multiple API groups are specified, any action requested against one of the enumerated
* resources in any API group will be allowed.
*/
final String[] apiGroups;
/**
* Resources is a list of resources this rule applies to. '*' represents all resources in
* the specified apiGroups.
* '*&#47;foo' represents the subresource 'foo' for all resources in the specified
* apiGroups.
*/
final String[] resources;
/**
* ResourceNames is an optional white list of names that the rule applies to. An empty set
* means that everything is allowed.
*/
final String[] resourceNames;
/**
* NonResourceURLs is a set of partial urls that a user should have access to.
* *s are allowed, but only as the full, final step in the path
* If an action is not a resource API request, then the URL is split on '/' and is checked
* against the NonResourceURLs to look for a match.
* Since non-resource URLs are not namespaced, this field is only applicable for
* ClusterRoles referenced from a ClusterRoleBinding.
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
* URL paths (such as "/api"), but not both.
*/
final String[] nonResourceURLs;
/**
* about who the rule applies to or which namespace the rule applies to.
*/
final String[] verbs;
public PolicyRule() {
this(null, null, null, null, null);
}
public PolicyRule(String[] apiGroups, String[] resources,
String[] resourceNames,
String[] nonResourceURLs, String[] verbs) {
this.apiGroups = nullElseEmpty(apiGroups);
this.resources = nullElseEmpty(resources);
this.resourceNames = nullElseEmpty(resourceNames);
this.nonResourceURLs = nullElseEmpty(nonResourceURLs);
this.verbs = nullElseEmpty(verbs);
}
String[] nullElseEmpty(String... items) {
if (items == null) {
return new String[] {};
}
return items;
}
@Override
public int compareTo(@NonNull PolicyRule other) {
int result = compare(apiGroups, other.apiGroups);
if (result != 0) {
return result;
}
result = compare(resources, other.resources);
if (result != 0) {
return result;
}
result = compare(resourceNames, other.resourceNames);
if (result != 0) {
return result;
}
result = compare(nonResourceURLs, other.nonResourceURLs);
if (result != 0) {
return result;
}
result = compare(verbs, other.verbs);
return result;
}
public static class Builder {
String[] apiGroups;
String[] resources;
String[] resourceNames;
String[] nonResourceURLs;
String[] verbs;
public Builder apiGroups(String... apiGroups) {
this.apiGroups = apiGroups;
return this;
}
public Builder resources(String... resources) {
this.resources = resources;
return this;
}
public Builder resourceNames(String... resourceNames) {
this.resourceNames = resourceNames;
return this;
}
public Builder nonResourceURLs(String... nonResourceURLs) {
this.nonResourceURLs = nonResourceURLs;
return this;
}
public Builder verbs(String... verbs) {
this.verbs = verbs;
return this;
}
public PolicyRule build() {
return new PolicyRule(apiGroups, resources, resourceNames,
nonResourceURLs,
verbs);
}
}
}
}

View File

@ -0,0 +1,169 @@
package run.halo.app.core.extension;
import static run.halo.app.core.extension.RoleBinding.GROUP;
import static run.halo.app.core.extension.RoleBinding.KIND;
import static run.halo.app.core.extension.RoleBinding.VERSION;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.util.StringUtils;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.ExtensionOperator;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Metadata;
/**
* RoleBinding references a role, but does not contain it.
* It can reference a Role in the global.
* It adds who information via Subjects.
*
* @author guqing
* @since 2.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@GVK(group = GROUP,
version = VERSION,
kind = KIND,
plural = "rolebindings",
singular = "rolebinding")
public class RoleBinding extends AbstractExtension {
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "RoleBinding";
/**
* Subjects holds references to the objects the role applies to.
*/
List<Subject> subjects;
/**
* RoleRef can reference a Role in the current namespace or a ClusterRole in the global
* namespace.
* If the RoleRef cannot be resolved, the Authorizer must return an error.
*/
RoleRef roleRef;
/**
* RoleRef contains information that points to the role being used.
*
* @author guqing
* @since 2.0.0
*/
@Data
public static class RoleRef {
/**
* Kind is the type of resource being referenced.
*/
String kind;
/**
* Name is the name of resource being referenced.
*/
String name;
/**
* APIGroup is the group for the resource being referenced.
*/
String apiGroup;
}
/**
* @author guqing
* @since 2.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Subject {
/**
* Kind of object being referenced. Values defined by this API group are "User", "Group",
* and "ServiceAccount".
* If the Authorizer does not recognize the kind value, the Authorizer should report
* an error.
*/
String kind;
/**
* Name of the object being referenced.
*/
String name;
/**
* APIGroup holds the API group of the referenced subject.
* Defaults to "" for ServiceAccount subjects.
* Defaults to "rbac.authorization.halo.run" for User and Group subjects.
*/
String apiGroup;
public static Predicate<Subject> isUser(String username) {
return subject -> User.KIND.equals(subject.getKind())
&& User.GROUP.equals(subject.getApiGroup())
&& username.equals(subject.getName());
}
public static Predicate<Subject> containsUser(Set<String> usernames) {
return subject -> User.KIND.equals(subject.getKind())
&& User.GROUP.equals(subject.apiGroup)
&& usernames.contains(subject.getName());
}
@Override
public String toString() {
if (StringUtils.hasText(apiGroup)) {
return apiGroup + "/" + kind + "/" + name;
}
return kind + "/" + name;
}
}
public static RoleBinding create(String username, String roleName) {
var metadata = new Metadata();
metadata.setName(String.join("-", username, roleName, "binding"));
var roleRef = new RoleRef();
roleRef.setKind(Role.KIND);
roleRef.setName(roleName);
roleRef.setApiGroup(Role.GROUP);
var subject = new Subject();
subject.setKind(User.KIND);
subject.setName(username);
subject.setApiGroup(User.GROUP);
var binding = new RoleBinding();
binding.setMetadata(metadata);
binding.setRoleRef(roleRef);
// keep the subjects mutable
var subjects = new LinkedList<Subject>();
subjects.add(subject);
binding.setSubjects(subjects);
return binding;
}
public static Predicate<RoleBinding> containsUser(String username) {
return ExtensionOperator.<RoleBinding>isNotDeleted().and(
binding -> binding.getSubjects().stream()
.anyMatch(Subject.isUser(username)));
}
public static Predicate<RoleBinding> containsUser(Set<String> usernames) {
return ExtensionOperator.<RoleBinding>isNotDeleted()
.and(binding -> binding.getSubjects().stream()
.anyMatch(Subject.containsUser(usernames)));
}
}

View File

@ -0,0 +1,51 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.extension.GroupVersionKind.fromExtension;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
/**
* {@link Setting} is a custom extension to generate forms based on configuration.
*
* @author guqing
* @since 2.0.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = Setting.KIND,
plural = "settings", singular = "setting")
public class Setting extends AbstractExtension {
public static final String KIND = "Setting";
public static final GroupVersionKind GVK = fromExtension(Setting.class);
@Schema(requiredMode = REQUIRED)
private SettingSpec spec;
@Data
public static class SettingSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private List<SettingForm> forms;
}
@Data
public static class SettingForm {
@Schema(requiredMode = REQUIRED)
private String group;
private String label;
@Schema(requiredMode = REQUIRED)
private List<Object> formSchema;
}
}

View File

@ -0,0 +1,177 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.infra.ConditionList;
import run.halo.app.infra.model.License;
/**
* <p>Theme extension.</p>
*
* @author guqing
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "theme.halo.run", version = "v1alpha1", kind = Theme.KIND,
plural = "themes", singular = "theme")
public class Theme extends AbstractExtension {
public static final String KIND = "Theme";
public static final String THEME_NAME_LABEL = "theme.halo.run/theme-name";
@Schema(requiredMode = REQUIRED)
private ThemeSpec spec;
private ThemeStatus status;
@Data
@ToString
public static class ThemeSpec {
private static final String WILDCARD = "*";
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(requiredMode = REQUIRED)
private Author author;
private String description;
private String logo;
@Deprecated(forRemoval = true, since = "2.7.0")
private String website;
private String homepage;
private String repo;
private String issues;
private String version;
@Deprecated(forRemoval = true, since = "2.2.0")
@Schema(description = "Deprecated, use `requires` instead.")
private String require;
@Schema(requiredMode = NOT_REQUIRED)
private String requires;
private String settingName;
private String configMapName;
private List<License> license;
@Schema
private CustomTemplates customTemplates;
@NonNull
public String getVersion() {
return StringUtils.defaultString(this.version, WILDCARD);
}
/**
* if requires is not empty, then return requires, else return require or {@code WILDCARD}.
*
* @return requires to satisfies system version
*/
@NonNull
public String getRequires() {
if (StringUtils.isNotBlank(this.requires)) {
return this.requires;
}
return StringUtils.defaultString(this.require, WILDCARD);
}
/**
* Compatible with {@link #website} property.
*/
public String getHomepage() {
return StringUtils.defaultString(this.homepage, this.website);
}
}
@Data
public static class ThemeStatus {
private ThemePhase phase;
private ConditionList conditions;
private String location;
}
/**
* Null-safe get {@link ConditionList} from theme status.
*
* @param theme theme must not be null
* @return condition list
*/
public static ConditionList nullSafeConditionList(Theme theme) {
Assert.notNull(theme, "The theme must not be null");
ThemeStatus status = ObjectUtils.defaultIfNull(theme.getStatus(), new ThemeStatus());
theme.setStatus(status);
ConditionList conditions =
ObjectUtils.defaultIfNull(status.getConditions(), new ConditionList());
status.setConditions(conditions);
return conditions;
}
public enum ThemePhase {
READY,
FAILED,
UNKNOWN,
}
@Data
@ToString
public static class Author {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
private String website;
}
@Data
public static class CustomTemplates {
private List<TemplateDescriptor> post;
private List<TemplateDescriptor> category;
private List<TemplateDescriptor> page;
}
/**
* Type used to describe custom template page.
*
* @author guqing
* @since 2.0.0
*/
@Data
public static class TemplateDescriptor {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
private String description;
private String screenshot;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String file;
}
}

View File

@ -0,0 +1,117 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.User.GROUP;
import static run.halo.app.core.extension.User.KIND;
import static run.halo.app.core.extension.User.VERSION;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* The extension represents user details of Halo.
*
* @author johnniang
*/
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = GROUP,
version = VERSION,
kind = KIND,
singular = "user",
plural = "users")
public class User extends AbstractExtension {
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "User";
public static final String USER_RELATED_ROLES_INDEX = "roles";
public static final String ROLE_NAMES_ANNO = "rbac.authorization.halo.run/role-names";
public static final String EMAIL_TO_VERIFY = "halo.run/email-to-verify";
public static final String LAST_AVATAR_ATTACHMENT_NAME_ANNO =
"halo.run/last-avatar-attachment-name";
public static final String AVATAR_ATTACHMENT_NAME_ANNO = "halo.run/avatar-attachment-name";
public static final String HIDDEN_USER_LABEL = "halo.run/hidden-user";
public static final String REQUEST_TO_UPDATE = "halo.run/request-to-update";
@Schema(requiredMode = REQUIRED)
private UserSpec spec = new UserSpec();
private UserStatus status = new UserStatus();
@Data
public static class UserSpec {
@Schema(requiredMode = REQUIRED)
private String displayName;
private String avatar;
@Schema(requiredMode = REQUIRED)
private String email;
private boolean emailVerified;
private String phone;
private String password;
private String bio;
private Instant registeredAt;
private Boolean twoFactorAuthEnabled;
private String totpEncryptedSecret;
private Boolean disabled;
private Integer loginHistoryLimit;
}
@Data
public static class UserStatus {
private Instant lastLoginAt;
private String permalink;
private List<LoginHistory> loginHistories;
}
@Data
public static class LoginHistory {
@Schema(requiredMode = REQUIRED)
private Instant loginAt;
@Schema(requiredMode = REQUIRED)
private String sourceIp;
@Schema(requiredMode = REQUIRED)
private String userAgent;
@Schema(requiredMode = REQUIRED)
private Boolean successful;
private String reason;
}
}

View File

@ -0,0 +1,83 @@
package run.halo.app.core.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Metadata;
/**
* User connection extension.
*
* @author guqing
* @since 2.4.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "auth.halo.run", version = "v1alpha1", kind = "UserConnection",
singular = "userconnection", plural = "userconnections")
public class UserConnection extends AbstractExtension {
@Schema(requiredMode = REQUIRED)
private UserConnectionSpec spec;
@Data
public static class UserConnectionSpec {
/**
* The name of the OAuth provider (e.g. Google, Facebook, Twitter).
*/
@Schema(requiredMode = REQUIRED)
private String registrationId;
/**
* The {@link Metadata#getName()} of the user associated with the OAuth connection.
*/
@Schema(requiredMode = REQUIRED)
private String username;
/**
* The unique identifier for the user's connection to the OAuth provider.
* for example, the user's GitHub id.
*/
@Schema(requiredMode = REQUIRED)
private String providerUserId;
/**
* The display name for the user's connection to the OAuth provider.
*/
@Schema(requiredMode = REQUIRED)
private String displayName;
/**
* The URL to the user's profile page on the OAuth provider.
* For example, the user's GitHub profile URL.
*/
private String profileUrl;
/**
* The URL to the user's avatar image on the OAuth provider.
* For example, the user's GitHub avatar URL.
*/
private String avatarUrl;
/**
* The access token provided by the OAuth provider.
*/
@Schema(requiredMode = REQUIRED)
private String accessToken;
/**
* The refresh token provided by the OAuth provider (if applicable).
*/
private String refreshToken;
private Instant expiresAt;
private Instant updatedAt;
}
}

View File

@ -0,0 +1,68 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.Attachment.KIND;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,
plural = "attachments", singular = "attachment")
public class Attachment extends AbstractExtension {
public static final String KIND = "Attachment";
@Schema(requiredMode = REQUIRED)
private AttachmentSpec spec;
private AttachmentStatus status;
@Data
public static class AttachmentSpec {
@Schema(description = "Display name of attachment")
private String displayName;
@Schema(description = "Group name")
private String groupName;
@Schema(description = "Policy name")
private String policyName;
@Schema(description = "Name of User who uploads the attachment")
private String ownerName;
@Schema(description = "Media type of attachment")
private String mediaType;
@Schema(description = "Size of attachment. Unit is Byte", minimum = "0")
private Long size;
@ArraySchema(
arraySchema = @Schema(description = "Tags of attachment"),
schema = @Schema(description = "Tag name"))
private Set<String> tags;
}
@Data
public static class AttachmentStatus {
@Schema(description = """
Permalink of attachment.
If it is in local storage, the public URL will be set.
If it is in s3 storage, the Object URL will be set.
""")
private String permalink;
}
}

View File

@ -0,0 +1,28 @@
package run.halo.app.core.extension.attachment;
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
public enum Constant {
;
public static final String GROUP = "storage.halo.run";
public static final String VERSION = "v1alpha1";
/**
* The relative path starting from attachments folder is for deletion.
*/
public static final String LOCAL_REL_PATH_ANNO_KEY = GROUP + "/local-relative-path";
/**
* The encoded URI is for building external url.
*/
public static final String URI_ANNO_KEY = GROUP + "/uri";
/**
* Do not use this key to set external link. You could implement
* {@link AttachmentHandler#getPermalink} by your self.
* <p>
*/
public static final String EXTERNAL_LINK_ANNO_KEY = GROUP + "/external-link";
public static final String FINALIZER_NAME = "attachment-manager";
}

View File

@ -0,0 +1,48 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.Group.KIND;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,
plural = "groups", singular = "group")
public class Group extends AbstractExtension {
public static final String KIND = "Group";
public static final String HIDDEN_LABEL = "halo.run/hidden";
@Schema(requiredMode = REQUIRED)
private GroupSpec spec;
private GroupStatus status;
@Data
public static class GroupSpec {
@Schema(requiredMode = REQUIRED, description = "Display name of group")
private String displayName;
}
@Data
public static class GroupStatus {
@Schema(description = "Update timestamp of the group")
private Instant updateTimestamp;
@Schema(description = "Total of attachments under the current group", minimum = "0")
private Long totalAttachments;
}
}

View File

@ -0,0 +1,39 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.Policy.KIND;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,
plural = "policies", singular = "policy")
public class Policy extends AbstractExtension {
public static final String KIND = "Policy";
@Schema(requiredMode = REQUIRED)
private PolicySpec spec;
@Data
public static class PolicySpec {
@Schema(requiredMode = REQUIRED, description = "Display name of policy")
private String displayName;
@Schema(requiredMode = REQUIRED, description = "Reference name of PolicyTemplate")
private String templateName;
@Schema(description = "Reference name of ConfigMap extension")
private String configMapName;
}
}

View File

@ -0,0 +1,34 @@
package run.halo.app.core.extension.attachment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.attachment.PolicyTemplate.KIND;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,
plural = "policytemplates", singular = "policytemplate")
public class PolicyTemplate extends AbstractExtension {
public static final String KIND = "PolicyTemplate";
private PolicyTemplateSpec spec;
@Data
public static class PolicyTemplateSpec {
private String displayName;
@Schema(requiredMode = REQUIRED)
private String settingName;
}
}

View File

@ -0,0 +1,74 @@
package run.halo.app.core.extension.attachment.endpoint;
import java.net.URI;
import java.time.Duration;
import org.pf4j.ExtensionPoint;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Policy;
import run.halo.app.extension.ConfigMap;
public interface AttachmentHandler extends ExtensionPoint {
Mono<Attachment> upload(UploadContext context);
Mono<Attachment> delete(DeleteContext context);
/**
* Gets a shared URL which could be accessed publicly.
* 1. If the attachment is in local storage, the permalink will be returned.
* 2. If the attachment is in s3 storage, the Presigned URL will be returned.
* <p>
* Please note that the default implementation is only for back compatibility.
*
* @param attachment contains detail of attachment.
* @param policy is storage policy.
* @param configMap contains configuration needed by handler.
* @param ttl indicates how long the URL is alive.
* @return shared URL which could be accessed publicly. Might be relative URL.
*/
default Mono<URI> getSharedURL(Attachment attachment,
Policy policy,
ConfigMap configMap,
Duration ttl) {
return Mono.empty();
}
/**
* Gets a permalink representing a unique attachment.
* If the attachment is in local storage, the permalink will be returned.
* If the attachment is in s3 storage, the Object URL will be returned.
* <p>
* Please note that the default implementation is only for back compatibility.
*
* @param attachment contains detail of attachment.
* @param policy is storage policy.
* @param configMap contains configuration needed by handler.
* @return permalink representing a unique attachment. Might be relative URL.
*/
default Mono<URI> getPermalink(Attachment attachment,
Policy policy,
ConfigMap configMap) {
return Mono.empty();
}
interface UploadContext {
FilePart file();
Policy policy();
ConfigMap configMap();
}
interface DeleteContext {
Attachment attachment();
Policy policy();
ConfigMap configMap();
}
}

View File

@ -0,0 +1,9 @@
package run.halo.app.core.extension.attachment.endpoint;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Policy;
import run.halo.app.extension.ConfigMap;
public record DeleteOption(Attachment attachment, Policy policy, ConfigMap configMap)
implements AttachmentHandler.DeleteContext {
}

View File

@ -0,0 +1,40 @@
package run.halo.app.core.extension.attachment.endpoint;
import java.nio.file.Path;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* SimpleFilePart is an adapter of simple data for uploading.
*
* @param filename is name of the attachment file.
* @param content is binary data of the attachment file.
* @param mediaType is media type of the attachment file.
*/
public record SimpleFilePart(
String filename,
Flux<DataBuffer> content,
MediaType mediaType
) implements FilePart {
@Override
public Mono<Void> transferTo(Path dest) {
return DataBufferUtils.write(content(), dest);
}
@Override
public String name() {
return filename();
}
@Override
public HttpHeaders headers() {
var headers = new HttpHeaders();
headers.setContentType(mediaType);
return HttpHeaders.readOnlyHttpHeaders(headers);
}
}

View File

@ -0,0 +1,23 @@
package run.halo.app.core.extension.attachment.endpoint;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Flux;
import run.halo.app.core.extension.attachment.Policy;
import run.halo.app.extension.ConfigMap;
public record UploadOption(FilePart file,
Policy policy,
ConfigMap configMap) implements AttachmentHandler.UploadContext {
public static UploadOption from(String filename,
Flux<DataBuffer> content,
MediaType mediaType,
Policy policy,
ConfigMap configMap) {
var filePart = new SimpleFilePart(filename, content, mediaType);
return new UploadOption(filePart, policy, configMap);
}
}

View File

@ -0,0 +1,117 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.content.Category.KIND;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
/**
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION,
kind = KIND, plural = "categories", singular = "category")
@EqualsAndHashCode(callSuper = true)
public class Category extends AbstractExtension {
public static final String KIND = "Category";
public static final String LAST_HIDDEN_STATE_ANNO = "content.halo.run/last-hidden-state";
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Category.class);
@Schema(requiredMode = REQUIRED)
private CategorySpec spec;
@Schema
private CategoryStatus status;
@JsonIgnore
public boolean isDeleted() {
return getMetadata().getDeletionTimestamp() != null;
}
@Data
public static class CategorySpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
private String description;
private String cover;
@Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
private String template;
/**
* <p>Used to specify the template for the posts associated with the category.</p>
* <p>The priority is not as high as that of the post.</p>
* <p>If the post also specifies a template, the post's template will prevail.</p>
*/
@Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
private String postTemplate;
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
private List<String> children;
/**
* <p>if a category is queried for related posts, the default behavior is to
* query all posts under the category including its subcategories, but if this field is
* set to true, cascade query behavior will be terminated here.</p>
* <p>For example, if a category has subcategories A and B, and A has subcategories C and
* D and C marked this field as true, when querying posts under A category,all posts under A
* and B will be queried, but C and D will not be queried.</p>
*/
private boolean preventParentPostCascadeQuery;
/**
* <p>Whether to hide the category from the category list.</p>
* <p>When set to true, the category including its subcategories and related posts will
* not be displayed in the category list, but it can still be accessed by permalink.</p>
* <p>Limitation: It only takes effect on the theme-side categorized list and it only
* allows to be set to true on the first level(root node) of categories.</p>
*/
private boolean hideFromList;
}
@JsonIgnore
public CategoryStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new CategoryStatus();
}
return this.status;
}
@Data
public static class CategoryStatus {
private String permalink;
/**
* 包括当前和其下所有层级的文章数量 (depth=max).
*/
public Integer postCount;
/**
* 包括当前和其下所有层级的已发布且公开的文章数量 (depth=max).
*/
public Integer visiblePostCount;
}
}

View File

@ -0,0 +1,164 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Ref;
/**
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Comment.KIND,
plural = "comments", singular = "comment")
@EqualsAndHashCode(callSuper = true)
public class Comment extends AbstractExtension {
public static final String KIND = "Comment";
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";
@Schema(requiredMode = REQUIRED)
private CommentSpec spec;
@Schema
private CommentStatus status;
@JsonIgnore
public CommentStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new CommentStatus();
}
return this.status;
}
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public static class CommentSpec extends BaseCommentSpec {
@Schema(requiredMode = REQUIRED)
private Ref subjectRef;
private Instant lastReadTime;
}
@Data
public static class BaseCommentSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String raw;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String content;
@Schema(requiredMode = REQUIRED)
private CommentOwner owner;
private String userAgent;
private String ipAddress;
private Instant approvedTime;
/**
* The user-defined creation time default is <code>metadata.creationTimestamp</code>.
*/
private Instant creationTime;
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean top;
@Schema(requiredMode = REQUIRED, defaultValue = "true")
private Boolean allowNotification;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean approved;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean hidden;
}
@Data
public static class CommentOwner {
public static final String KIND_EMAIL = "Email";
public static final String AVATAR_ANNO = "avatar";
public static final String WEBSITE_ANNO = "website";
public static final String EMAIL_HASH_ANNO = "email-hash";
@Schema(requiredMode = REQUIRED, minLength = 1)
private String kind;
@Schema(requiredMode = REQUIRED, maxLength = 64)
private String name;
private String displayName;
private Map<String, String> annotations;
@Nullable
@JsonIgnore
public String getAnnotation(String key) {
return annotations == null ? null : annotations.get(key);
}
public static String ownerIdentity(String kind, String name) {
return kind + "#" + name;
}
}
@Data
public static class CommentStatus {
private Instant lastReplyTime;
private Integer replyCount;
private Integer visibleReplyCount;
private Integer unreadReplyCount;
private Boolean hasNewReply;
private Long observedVersion;
}
public static String toSubjectRefKey(Ref subjectRef) {
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
}
public static int getUnreadReplyCount(List<Reply> replies, Instant lastReadTime) {
if (CollectionUtils.isEmpty(replies)) {
return 0;
}
long unreadReplyCount = replies.stream()
.filter(existingReply -> {
if (lastReadTime == null) {
return true;
}
Instant creationTime = defaultIfNull(existingReply.getSpec().getCreationTime(),
existingReply.getMetadata().getCreationTimestamp());
return creationTime.isAfter(lastReadTime);
})
.count();
return (int) unreadReplyCount;
}
}

View File

@ -0,0 +1,13 @@
package run.halo.app.core.extension.content;
public enum Constant {
;
public static final String GROUP = "content.halo.run";
public static final String VERSION = "v1alpha1";
public static final String LAST_READ_TIME_ANNO = "content.halo.run/last-read-time";
public static final String PERMALINK_PATTERN_ANNO = "content.halo.run/permalink-pattern";
public static final String CHECKSUM_CONFIG_ANNO = "checksum/config";
}

View File

@ -0,0 +1,245 @@
package run.halo.app.core.extension.content;
import static java.lang.Boolean.parseBoolean;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.infra.ConditionList;
/**
* <p>Post extension.</p>
*
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Post.KIND,
plural = "posts", singular = "post")
@EqualsAndHashCode(callSuper = true)
public class Post extends AbstractExtension {
public static final String KIND = "Post";
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Post.class);
public static final String CATEGORIES_ANNO = "content.halo.run/categories";
public static final String LAST_RELEASED_SNAPSHOT_ANNO =
"content.halo.run/last-released-snapshot";
public static final String LAST_ASSOCIATED_TAGS_ANNO = "content.halo.run/last-associated-tags";
public static final String LAST_ASSOCIATED_CATEGORIES_ANNO =
"content.halo.run/last-associated-categories";
public static final String STATS_ANNO = "content.halo.run/stats";
/**
* <p>The key of the label that indicates that the post is scheduled to be published.</p>
* <p>Can be used to query posts that are scheduled to be published.</p>
*/
public static final String SCHEDULING_PUBLISH_LABEL = "content.halo.run/scheduling-publish";
public static final String DELETED_LABEL = "content.halo.run/deleted";
public static final String PUBLISHED_LABEL = "content.halo.run/published";
public static final String OWNER_LABEL = "content.halo.run/owner";
public static final String VISIBLE_LABEL = "content.halo.run/visible";
public static final String ARCHIVE_YEAR_LABEL = "content.halo.run/archive-year";
public static final String ARCHIVE_MONTH_LABEL = "content.halo.run/archive-month";
public static final String ARCHIVE_DAY_LABEL = "content.halo.run/archive-day";
@Schema(requiredMode = RequiredMode.REQUIRED)
private PostSpec spec;
@Schema
private PostStatus status;
@JsonIgnore
public PostStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new PostStatus();
}
return status;
}
@JsonIgnore
public boolean isDeleted() {
return Objects.equals(true, spec.getDeleted())
|| getMetadata().getDeletionTimestamp() != null;
}
@JsonIgnore
public boolean isPublished() {
return isPublished(this.getMetadata());
}
public static boolean isPublished(MetadataOperator metadata) {
var labels = metadata.getLabels();
return labels != null && parseBoolean(labels.getOrDefault(PUBLISHED_LABEL, "false"));
}
public static boolean isRecycled(MetadataOperator metadata) {
var labels = metadata.getLabels();
return labels != null && parseBoolean(labels.getOrDefault(DELETED_LABEL, "false"));
}
public static boolean isPublic(PostSpec spec) {
return spec.getVisible() == null || VisibleEnum.PUBLIC.equals(spec.getVisible());
}
@Data
public static class PostSpec {
@Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)
private String title;
@Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)
private String slug;
/**
* 文章引用到的已发布的内容用于主题端显示.
*/
private String releaseSnapshot;
private String headSnapshot;
private String baseSnapshot;
private String owner;
private String template;
private String cover;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
private Boolean deleted;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
private Boolean publish;
private Instant publishTime;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
private Boolean pinned;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true")
private Boolean allowComment;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "PUBLIC")
private VisibleEnum visible;
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "0")
private Integer priority;
@Schema(requiredMode = RequiredMode.REQUIRED)
private Excerpt excerpt;
private List<String> categories;
private List<String> tags;
private List<Map<String, String>> htmlMetas;
}
@Data
public static class PostStatus {
@Schema(requiredMode = RequiredMode.REQUIRED)
private String phase;
@Schema
private ConditionList conditions;
private String permalink;
private String excerpt;
private Boolean inProgress;
private Integer commentsCount;
private List<String> contributors;
/**
* see {@link Category.CategorySpec#isHideFromList()}.
*/
private Boolean hideFromList;
private Instant lastModifyTime;
private Long observedVersion;
@JsonIgnore
public ConditionList getConditionsOrDefault() {
if (this.conditions == null) {
this.conditions = new ConditionList();
}
return conditions;
}
}
@Data
public static class Excerpt {
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true")
private Boolean autoGenerate;
private String raw;
}
public enum PostPhase {
DRAFT,
PENDING_APPROVAL,
PUBLISHED,
FAILED;
/**
* Convert string value to {@link PostPhase}.
*
* @param value enum value string
* @return {@link PostPhase} if found, otherwise null
*/
public static PostPhase from(String value) {
for (PostPhase phase : PostPhase.values()) {
if (phase.name().equalsIgnoreCase(value)) {
return phase;
}
}
return null;
}
}
public enum VisibleEnum {
PUBLIC,
INTERNAL,
PRIVATE;
/**
* Convert value string to {@link VisibleEnum}.
*
* @param value enum value string
* @return {@link VisibleEnum} if found, otherwise null
*/
public static VisibleEnum from(String value) {
for (VisibleEnum visible : VisibleEnum.values()) {
if (visible.name().equalsIgnoreCase(value)) {
return visible;
}
}
return null;
}
}
}

View File

@ -0,0 +1,56 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Reply.KIND,
plural = "replies", singular = "reply")
@EqualsAndHashCode(callSuper = true)
public class Reply extends AbstractExtension {
public static final String KIND = "Reply";
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";
@Schema(requiredMode = REQUIRED)
private ReplySpec spec;
@Schema
@Getter(onMethod_ = @NonNull)
private Status status = new Status();
@Data
@EqualsAndHashCode(callSuper = true)
public static class ReplySpec extends Comment.BaseCommentSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String commentName;
private String quoteReply;
}
@Data
@Schema(name = "ReplyStatus")
public static class Status {
private Long observedVersion;
}
public void setStatus(Status status) {
this.status = status == null ? new Status() : status;
}
}

View File

@ -0,0 +1,120 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.MetadataUtil;
/**
* <p>Single page extension.</p>
*
* @author guqing
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = SinglePage.KIND,
plural = "singlepages", singular = "singlepage")
@EqualsAndHashCode(callSuper = true)
public class SinglePage extends AbstractExtension {
public static final String KIND = "SinglePage";
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(SinglePage.class);
public static final String DELETED_LABEL = "content.halo.run/deleted";
public static final String PUBLISHED_LABEL = "content.halo.run/published";
public static final String LAST_RELEASED_SNAPSHOT_ANNO =
"content.halo.run/last-released-snapshot";
public static final String OWNER_LABEL = "content.halo.run/owner";
public static final String VISIBLE_LABEL = "content.halo.run/visible";
@Schema(requiredMode = REQUIRED)
private SinglePageSpec spec;
@Schema
private SinglePageStatus status;
@JsonIgnore
public SinglePageStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new SinglePageStatus();
}
return this.status;
}
@JsonIgnore
public boolean isPublished() {
Map<String, String> labels = getMetadata().getLabels();
return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true");
}
@Data
public static class SinglePageSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String title;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
/**
* 引用到的已发布的内容用于主题端显示.
*/
private String releaseSnapshot;
private String headSnapshot;
private String baseSnapshot;
private String owner;
private String template;
private String cover;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean deleted;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean publish;
private Instant publishTime;
@Schema(requiredMode = REQUIRED, defaultValue = "false")
private Boolean pinned;
@Schema(requiredMode = REQUIRED, defaultValue = "true")
private Boolean allowComment;
@Schema(requiredMode = REQUIRED, defaultValue = "PUBLIC")
private Post.VisibleEnum visible;
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
@Schema(requiredMode = REQUIRED)
private Post.Excerpt excerpt;
private List<Map<String, String>> htmlMetas;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class SinglePageStatus extends Post.PostStatus {
}
public static void changePublishedState(SinglePage page, boolean value) {
Map<String, String> labels = MetadataUtil.nullSafeLabels(page);
labels.put(PUBLISHED_LABEL, String.valueOf(value));
}
}

View File

@ -0,0 +1,90 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.LinkedHashSet;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Ref;
/**
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Snapshot.KIND,
plural = "snapshots", singular = "snapshot")
@EqualsAndHashCode(callSuper = true)
public class Snapshot extends AbstractExtension {
public static final String KIND = "Snapshot";
public static final String KEEP_RAW_ANNO = "content.halo.run/keep-raw";
public static final String PATCHED_CONTENT_ANNO = "content.halo.run/patched-content";
public static final String PATCHED_RAW_ANNO = "content.halo.run/patched-raw";
@Schema(requiredMode = REQUIRED)
private SnapShotSpec spec;
@Data
public static class SnapShotSpec {
@Schema(requiredMode = REQUIRED)
private Ref subjectRef;
/**
* such as: markdown | html | json | asciidoc | latex.
*/
@Schema(requiredMode = REQUIRED, minLength = 1, maxLength = 50)
private String rawType;
private String rawPatch;
private String contentPatch;
private String parentSnapshotName;
private Instant lastModifyTime;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String owner;
private Set<String> contributors;
}
public static void addContributor(Snapshot snapshot, String name) {
Assert.notNull(name, "The username must not be null.");
Set<String> contributors = snapshot.getSpec().getContributors();
if (contributors == null) {
contributors = new LinkedHashSet<>();
snapshot.getSpec().setContributors(contributors);
}
contributors.add(name);
}
/**
* Check if the given snapshot is a base snapshot.
*
* @param snapshot must not be null.
* @return true if the given snapshot is a base snapshot; false otherwise.
*/
public static boolean isBaseSnapshot(@NonNull Snapshot snapshot) {
var annotations = snapshot.getMetadata().getAnnotations();
if (annotations == null) {
return false;
}
return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO));
}
public static String toSubjectRefKey(Ref subjectRef) {
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
}
}

View File

@ -0,0 +1,85 @@
package run.halo.app.core.extension.content;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
/**
* @author guqing
* @see <a href="https://github.com/halo-dev/halo/issues/2322">issue#2322</a>
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@GVK(group = Constant.GROUP, version = Constant.VERSION,
kind = Tag.KIND, plural = "tags", singular = "tag")
@EqualsAndHashCode(callSuper = true)
public class Tag extends AbstractExtension {
public static final String KIND = "Tag";
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Tag.class);
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";
@Schema(requiredMode = REQUIRED)
private TagSpec spec;
@Schema
private TagStatus status;
@Data
public static class TagSpec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String slug;
/**
* Color regex explanation.
* <pre>
* ^ # start of the line
* # # start with a number sign `#`
* ( # start of (group 1)
* [a-fA-F0-9]{6} # support z-f, A-F and 0-9, with a length of 6
* | # or
* [a-fA-F0-9]{3} # support z-f, A-F and 0-9, with a length of 3
* ) # end of (group 1)
* $ # end of the line
* </pre>
*/
@Schema(pattern = "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$")
private String color;
private String cover;
}
@JsonIgnore
public TagStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new TagStatus();
}
return this.status;
}
@Data
public static class TagStatus {
private String permalink;
public Integer visiblePostCount;
public Integer postCount;
private Long observedVersion;
}
}

View File

@ -0,0 +1,20 @@
package run.halo.app.core.extension.endpoint;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.GroupVersion;
/**
* RouterFunction provider for custom endpoints.
*
* @author johnniang
*/
public interface CustomEndpoint {
RouterFunction<ServerResponse> endpoint();
default GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("api.console.halo.run/v1alpha1");
}
}

View File

@ -0,0 +1,31 @@
package run.halo.app.core.extension.endpoint;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
public interface SortResolver {
SortResolver defaultInstance = new DefaultSortResolver();
@NonNull
Sort resolve(@NonNull ServerWebExchange exchange);
class DefaultSortResolver extends ReactiveSortHandlerMethodArgumentResolver
implements SortResolver {
@Override
@NonNull
protected Sort getDefaultFromAnnotationOrFallback(@Nullable MethodParameter parameter) {
return Sort.unsorted();
}
@Override
public Sort resolve(ServerWebExchange exchange) {
return resolveArgumentValue(null, null, exchange);
}
}
}

View File

@ -0,0 +1,58 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>{@link Notification} is a custom extension that used to store notification information for
* inner use, it's on-site notification.</p>
*
* <p>Supports the following operations:</p>
* <ul>
* <li>Marked as read: {@link NotificationSpec#setUnread(boolean)}</li>
* <li>Get the last read time: {@link NotificationSpec#getLastReadAt()}</li>
* <li>Filter by recipient: {@link NotificationSpec#getRecipient()}</li>
* </ul>
*
* @author guqing
* @see Reason
* @see ReasonType
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "Notification", plural =
"notifications", singular = "notification")
public class Notification extends AbstractExtension {
@Schema
private NotificationSpec spec;
@Data
public static class NotificationSpec {
@Schema(requiredMode = REQUIRED, minLength = 1, description = "The name of user")
private String recipient;
@Schema(requiredMode = REQUIRED, minLength = 1, description = "The name of reason")
private String reason;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String title;
@Schema(requiredMode = REQUIRED)
private String rawContent;
@Schema(requiredMode = REQUIRED)
private String htmlContent;
private boolean unread;
private Instant lastReadAt;
}
}

View File

@ -0,0 +1,59 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>{@link NotificationTemplate} is a custom extension that defines a notification template.</p>
* <p>It describes the notification template's name, description, and the template content.</p>
* <p>{@link Spec#getReasonSelector()} is used to select the template by reasonType and language,
* if multiple templates are matched, the best match will be selected. This is useful when you
* want to override the default template.</p>
*
* @author guqing
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "NotificationTemplate",
plural = "notificationtemplates", singular = "notificationtemplate")
public class NotificationTemplate extends AbstractExtension {
@Schema
private Spec spec;
@Data
@Schema(name = "NotificationTemplateSpec")
public static class Spec {
@Schema
private ReasonSelector reasonSelector;
@Schema
private Template template;
}
@Data
@Schema(name = "TemplateContent")
public static class Template {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String title;
private String htmlBody;
private String rawBody;
}
@Data
public static class ReasonSelector {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String reasonType;
@Schema(requiredMode = REQUIRED, minLength = 1, defaultValue = "default")
private String language;
}
}

View File

@ -0,0 +1,54 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>{@link NotifierDescriptor} is a custom extension that defines a notifier.</p>
* <p>It describes the notifier's name, description, and the extension name of the notifier to
* let the user know what the notifier is and what it can do in the UI and also let the
* {@code NotificationCenter} know how to load the notifier and prepare the notifier's settings.</p>
*
* @author guqing
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "NotifierDescriptor",
plural = "notifierDescriptors", singular = "notifierDescriptor")
public class NotifierDescriptor extends AbstractExtension {
@Schema
private Spec spec;
@Data
@Schema(name = "NotifierDescriptorSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
private String description;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String notifierExtName;
private SettingRef senderSettingRef;
private SettingRef receiverSettingRef;
}
@Data
@Schema(name = "NotifierSettingRef")
public static class SettingRef {
@Schema(requiredMode = REQUIRED)
private String name;
@Schema(requiredMode = REQUIRED)
private String group;
}
}

View File

@ -0,0 +1,72 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.notification.ReasonAttributes;
/**
* <p>{@link Reason} is a custom extension that defines a reason for a notification, It represents
* an instance of a {@link ReasonType}.</p>
* <p>It can be understood as an event that triggers a notification.</p>
*
* @author guqing
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "Reason", plural =
"reasons", singular = "reason")
public class Reason extends AbstractExtension {
@Schema
private Spec spec;
@Data
@Accessors(chain = true)
@Schema(name = "ReasonSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED)
private String reasonType;
@Schema(requiredMode = REQUIRED)
private Subject subject;
@Schema(requiredMode = REQUIRED)
private String author;
@Schema(implementation = ReasonAttributes.class, requiredMode = NOT_REQUIRED,
description = "Attributes used to transfer data")
private ReasonAttributes attributes;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ReasonSubject")
public static class Subject {
@Schema(requiredMode = REQUIRED)
private String apiVersion;
@Schema(requiredMode = REQUIRED)
private String kind;
@Schema(requiredMode = REQUIRED)
private String name;
@Schema(requiredMode = REQUIRED)
private String title;
private String url;
}
}

View File

@ -0,0 +1,58 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>{@link ReasonType} is a custom extension that defines a type of reason.</p>
* <p>One {@link ReasonType} can have multiple {@link Reason}s to notify.</p>
*
* @author guqing
* @see NotificationTemplate
* @see Reason
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "ReasonType",
plural = "reasontypes", singular = "reasontype")
public class ReasonType extends AbstractExtension {
public static final String LOCALIZED_RESOURCE_NAME_ANNO =
"notification.halo.run/localized-resource-name";
@Schema
private Spec spec;
@Data
@Schema(name = "ReasonTypeSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String displayName;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String description;
private List<ReasonProperty> properties;
}
@Data
public static class ReasonProperty {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String type;
private String description;
@Schema(defaultValue = "false")
private boolean optional;
}
}

View File

@ -0,0 +1,144 @@
package run.halo.app.core.extension.notification;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.apache.commons.lang3.StringUtils.defaultString;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* <p>{@link Subscription} is a custom extension that defines a subscriber to be notified when a
* certain {@link Reason} is triggered.</p>
* <p>It holds a {@link Subscriber} to the user to be notified, a {@link InterestReason} to
* subscribe to.</p>
*
* @author guqing
* @since 2.10.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "notification.halo.run", version = "v1alpha1", kind = "Subscription",
plural = "subscriptions", singular = "subscription")
public class Subscription extends AbstractExtension {
@Schema
private Spec spec;
@Data
@Schema(name = "SubscriptionSpec")
public static class Spec {
@Schema(requiredMode = REQUIRED, description = "The subscriber to be notified")
private Subscriber subscriber;
@Schema(requiredMode = REQUIRED, description = "The token to unsubscribe")
private String unsubscribeToken;
@Schema(requiredMode = REQUIRED, description = "The reason to be interested in")
private InterestReason reason;
@Schema(description = "Perhaps users need to unsubscribe and "
+ "interact without receiving notifications again")
private boolean disabled;
}
@Data
public static class InterestReason {
@Schema(requiredMode = REQUIRED, description = "The name of the reason definition to be "
+ "interested in")
private String reasonType;
@Schema(requiredMode = REQUIRED, description = "The subject name of reason type to be"
+ " interested in")
private ReasonSubject subject;
@Schema(requiredMode = NOT_REQUIRED, description = "The expression to be interested in")
private String expression;
/**
* <p>Since 2.15.0, we have added a new field <code>expression</code> to the
* <code>InterestReason</code> object, so <code>subject</code> can be null.</p>
* <p>In this particular scenario, when the <code>subject</code> is null, we assign it a
* default <code>ReasonSubject</code> object. The properties of this object are set to
* specific values that do not occur in actual applications, thus we can consider this as
* <code>nonexistent data</code>.
* The purpose of this approach is to maintain backward compatibility, even if the
* <code>subject</code> can be null in the new version of the code.</p>
*/
public static void ensureSubjectHasValue(InterestReason interestReason) {
if (interestReason.getSubject() == null) {
interestReason.setSubject(createFallbackSubject());
}
}
/**
* Check if the given reason subject is a fallback subject.
*/
public static boolean isFallbackSubject(ReasonSubject reasonSubject) {
if (reasonSubject == null) {
return true;
}
var fallback = createFallbackSubject();
return fallback.getKind().equals(reasonSubject.getKind())
&& fallback.getApiVersion().equals(reasonSubject.getApiVersion());
}
static ReasonSubject createFallbackSubject() {
return ReasonSubject.builder()
.apiVersion("notification.halo.run/v1alpha1")
.kind("NonexistentKind")
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "InterestReasonSubject")
public static class ReasonSubject {
@Schema(requiredMode = NOT_REQUIRED, description = "if name is not specified, it presents "
+ "all subjects of the specified reason type and custom resources")
private String name;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String apiVersion;
@Schema(requiredMode = REQUIRED, minLength = 1)
private String kind;
@Override
public String toString() {
return kind + "#" + apiVersion + "/" + defaultString(name);
}
}
@Data
@Schema(name = "SubscriptionSubscriber")
public static class Subscriber {
@Schema(requiredMode = REQUIRED, minLength = 1)
private String name;
@Override
public String toString() {
return name;
}
}
/**
* Generate unsubscribe token for unsubscribe.
*
* @return unsubscribe token
*/
public static String generateUnsubscribeToken() {
return UUID.randomUUID().toString();
}
}

View File

@ -0,0 +1,94 @@
package run.halo.app.core.extension.service;
import java.net.URI;
import java.time.Duration;
import java.util.function.Consumer;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.attachment.Attachment;
/**
* AttachmentService.
*
* @author johnniang
* @since 2.5.0
*/
public interface AttachmentService {
/**
* Uploads the given attachment to specific storage using handlers in plugins.
* <p>
* If no handler can be found to upload the given attachment, ServerError exception will be
* thrown.
*
* @param policyName is attachment policy name.
* @param groupName is group name the attachment belongs.
* @param filePart contains filename, content and media type.
* @param beforeCreating is an attachment modifier before creating.
* @return attachment.
*/
Mono<Attachment> upload(
@NonNull String username,
@NonNull String policyName,
@Nullable String groupName,
@NonNull FilePart filePart,
@Nullable Consumer<Attachment> beforeCreating);
/**
* Uploads the given attachment to specific storage using handlers in plugins. Please note
* that we will make sure the request is authenticated, or an unauthorized exception throws.
* <p>
* If no handler can be found to upload the given attachment, ServerError exception will be
* thrown.
*
* @param policyName is attachment policy name.
* @param groupName is group name the attachment belongs.
* @param filename is filename of the attachment.
* @param content is binary data of the attachment.
* @param mediaType is media type of the attachment.
* @return attachment.
*/
Mono<Attachment> upload(@NonNull String policyName,
@Nullable String groupName,
@NonNull String filename,
@NonNull Flux<DataBuffer> content,
@Nullable MediaType mediaType);
/**
* Deletes an attachment using handlers in plugins.
* <p>
* If no handler can be found to delete the given attachment, Mono.empty() will return.
*
* @param attachment is to be deleted.
* @return deleted attachment.
*/
Mono<Attachment> delete(Attachment attachment);
/**
* Gets permalink using handlers in plugins.
* <p>
* If no handler can be found to delete the given attachment, Mono.empty() will return.
*
* @param attachment is created attachment.
* @return permalink
*/
Mono<URI> getPermalink(Attachment attachment);
/**
* Gets shared URL using handlers in plugins.
* <p>
* If no handler can be found to delete the given attachment, Mono.empty() will return.
*
* @param attachment is created attachment.
* @param ttl is time to live of the shared URL.
* @return time-to-live shared URL. Please note that, if the attachment is stored in local, the
* shared URL is equal to permalink.
*/
Mono<URI> getSharedURL(Attachment attachment, Duration ttl);
}

View File

@ -0,0 +1,24 @@
package run.halo.app.event.post;
import run.halo.app.core.extension.content.Post;
import run.halo.app.plugin.SharedEvent;
@SharedEvent
public class PostDeletedEvent extends PostEvent {
private final Post post;
public PostDeletedEvent(Object source, Post post) {
super(source, post.getMetadata().getName());
this.post = post;
}
/**
* Get original post.
*
* @return original post.
*/
public Post getPost() {
return post;
}
}

View File

@ -0,0 +1,27 @@
package run.halo.app.event.post;
import org.springframework.context.ApplicationEvent;
/**
* An abstract class for post events.
*
* @author johnniang
*/
public abstract class PostEvent extends ApplicationEvent {
private final String name;
public PostEvent(Object source, String name) {
super(source);
this.name = name;
}
/**
* Gets post metadata name.
*
* @return post metadata name
*/
public String getName() {
return name;
}
}

View File

@ -0,0 +1,12 @@
package run.halo.app.event.post;
import run.halo.app.plugin.SharedEvent;
@SharedEvent
public class PostPublishedEvent extends PostEvent {
public PostPublishedEvent(Object source, String postName) {
super(source, postName);
}
}

View File

@ -0,0 +1,12 @@
package run.halo.app.event.post;
import run.halo.app.plugin.SharedEvent;
@SharedEvent
public class PostUnpublishedEvent extends PostEvent {
public PostUnpublishedEvent(Object source, String postName) {
super(source, postName);
}
}

View File

@ -0,0 +1,12 @@
package run.halo.app.event.post;
import run.halo.app.plugin.SharedEvent;
@SharedEvent
public class PostUpdatedEvent extends PostEvent {
public PostUpdatedEvent(Object source, String postName) {
super(source, postName);
}
}

View File

@ -0,0 +1,30 @@
package run.halo.app.event.post;
import org.springframework.lang.Nullable;
import run.halo.app.core.extension.content.Post;
import run.halo.app.plugin.SharedEvent;
@SharedEvent
public class PostVisibleChangedEvent extends PostEvent {
@Nullable
private final Post.VisibleEnum oldVisible;
private final Post.VisibleEnum newVisible;
public PostVisibleChangedEvent(Object source, String postName,
@Nullable Post.VisibleEnum oldVisible, Post.VisibleEnum newVisible) {
super(source, postName);
this.oldVisible = oldVisible;
this.newVisible = newVisible;
}
@Nullable
public Post.VisibleEnum getOldVisible() {
return oldVisible;
}
public Post.VisibleEnum getNewVisible() {
return newVisible;
}
}

View File

@ -0,0 +1,30 @@
package run.halo.app.extension;
import lombok.Data;
/**
* AbstractExtension contains basic structure of Extension and implements the Extension interface.
*
* @author johnniang
*/
@Data
public abstract class AbstractExtension implements Extension {
private String apiVersion;
private String kind;
private MetadataOperator metadata;
@Override
public String getApiVersion() {
var apiVersionFromGvk = Extension.super.getApiVersion();
return apiVersionFromGvk != null ? apiVersionFromGvk : this.apiVersion;
}
@Override
public String getKind() {
var kindFromGvk = Extension.super.getKind();
return kindFromGvk != null ? kindFromGvk : this.kind;
}
}

View File

@ -0,0 +1,37 @@
package run.halo.app.extension;
import java.time.Instant;
import java.util.Comparator;
public enum Comparators {
;
public static <E extends Extension> Comparator<E> compareCreationTimestamp(boolean asc) {
var comparator =
Comparator.<E, Instant>comparing(e -> e.getMetadata().getCreationTimestamp());
return asc ? comparator : comparator.reversed();
}
public static <E extends Extension> Comparator<E> compareName(boolean asc) {
var comparator = Comparator.<E, String>comparing(e -> e.getMetadata().getName());
return asc ? comparator : comparator.reversed();
}
public static <T extends Extension> Comparator<T> defaultComparator() {
Comparator<T> comparator = compareCreationTimestamp(false);
comparator = comparator.thenComparing(compareName(true));
return comparator;
}
/**
* Get a nulls comparator.
*
* @param isAscending is ascending
* @return if ascending, return nulls high, else return nulls low
*/
public static Comparator<Object> nullsComparator(boolean isAscending) {
return isAscending
? org.springframework.util.comparator.Comparators.nullsHigh()
: org.springframework.util.comparator.Comparators.nullsLow();
}
}

View File

@ -0,0 +1,33 @@
package run.halo.app.extension;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* <p>ConfigMap holds configuration data to consume.</p>
*
* @author guqing
* @since 2.0.0
*/
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "", version = "v1alpha1", kind = ConfigMap.KIND, plural = "configmaps",
singular = "configmap")
public class ConfigMap extends AbstractExtension {
public static final String KIND = "ConfigMap";
private Map<String, String> data;
public ConfigMap putDataItem(String key, String dataItem) {
if (this.data == null) {
this.data = new LinkedHashMap<>();
}
this.data.put(key, dataItem);
return this;
}
}

View File

@ -0,0 +1,57 @@
package run.halo.app.extension;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.util.CollectionUtils;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
@Getter
@RequiredArgsConstructor
@Builder(builderMethodName = "internalBuilder")
public class DefaultExtensionMatcher implements ExtensionMatcher {
private final ExtensionClient client;
private final GroupVersionKind gvk;
private final LabelSelector labelSelector;
private final FieldSelector fieldSelector;
public static DefaultExtensionMatcherBuilder builder(ExtensionClient client,
GroupVersionKind gvk) {
return internalBuilder().client(client).gvk(gvk);
}
/**
* Match the given extension with the current matcher.
*
* @param extension extension to match
* @return true if the extension matches the current matcher
*/
@Override
public boolean match(Extension extension) {
if (!gvk.equals(extension.groupVersionKind())) {
return false;
}
if (!hasFieldSelector() && !hasLabelSelector()) {
return true;
}
var listOptions = new ListOptions();
listOptions.setLabelSelector(labelSelector);
var fieldQuery = QueryFactory.all();
if (hasFieldSelector()) {
fieldQuery = QueryFactory.and(fieldQuery, fieldSelector.query());
}
listOptions.setFieldSelector(new FieldSelector(fieldQuery));
return client.indexedQueryEngine().retrieve(getGvk(),
listOptions, PageRequestImpl.ofSize(1)).getTotal() > 0;
}
boolean hasFieldSelector() {
return fieldSelector != null && fieldSelector.query() != null;
}
boolean hasLabelSelector() {
return labelSelector != null && !CollectionUtils.isEmpty(labelSelector.getMatchers());
}
}

View File

@ -0,0 +1,23 @@
package run.halo.app.extension;
import java.util.Comparator;
import java.util.Objects;
/**
* Extension is an interface which represents an Extension. It contains setters and getters of
* GroupVersionKind and Metadata.
*/
public interface Extension extends ExtensionOperator, Comparable<Extension> {
@Override
default int compareTo(Extension another) {
if (another == null || another.getMetadata() == null) {
return 1;
}
if (getMetadata() == null) {
return -1;
}
return Objects.compare(getMetadata().getName(), another.getMetadata().getName(),
Comparator.naturalOrder());
}
}

View File

@ -0,0 +1,96 @@
package run.halo.app.extension;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import org.springframework.data.domain.Sort;
import run.halo.app.extension.index.IndexedQueryEngine;
/**
* ExtensionClient is an interface which contains some operations on Extension instead of
* ExtensionStore.
* <br/><br/>
* Please note that this client can only use in non-reactive environment. If you want to
* use Extension client in reactive environment, please use {@link ReactiveExtensionClient} instead.
*
* @author johnniang
*/
public interface ExtensionClient {
/**
* Lists Extensions by Extension type, filter and sorter.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param <E> is Extension type.
* @return all filtered and sorted Extensions.
*/
<E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator);
/**
* Lists Extensions by Extension type, filter, sorter and page info.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param page is page number which starts from 0.
* @param size is page size.
* @param <E> is Extension type.
* @return a list of Extensions.
*/
<E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator, int page, int size);
<E extends Extension> List<E> listAll(Class<E> type, ListOptions options, Sort sort);
<E extends Extension> ListResult<E> listBy(Class<E> type, ListOptions options,
PageRequest page);
/**
* Fetches Extension by its type and name.
*
* @param type is Extension type.
* @param name is Extension name.
* @param <E> is Extension type.
* @return an optional Extension.
*/
<E extends Extension> Optional<E> fetch(Class<E> type, String name);
Optional<Unstructured> fetch(GroupVersionKind gvk, String name);
/**
* Creates an Extension.
*
* @param extension is fresh Extension to be created. Please make sure the Extension name does
* not exist.
* @param <E> is Extension type.
*/
<E extends Extension> void create(E extension);
/**
* Updates an Extension.
*
* @param extension is an Extension to be updated. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> void update(E extension);
/**
* Deletes an Extension.
*
* @param extension is an Extension to be deleted. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> void delete(E extension);
IndexedQueryEngine indexedQueryEngine();
void watch(Watcher watcher);
}

View File

@ -0,0 +1,23 @@
package run.halo.app.extension;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
public interface ExtensionMatcher {
@Deprecated(since = "2.17.0", forRemoval = true)
default GroupVersionKind getGvk() {
return null;
}
@Deprecated(since = "2.17.0", forRemoval = true)
default LabelSelector getLabelSelector() {
return null;
}
@Deprecated(since = "2.17.0", forRemoval = true)
default FieldSelector getFieldSelector() {
return null;
}
boolean match(Extension extension);
}

View File

@ -0,0 +1,80 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.function.Predicate;
import org.springframework.util.StringUtils;
/**
* ExtensionOperator contains some getters and setters for required fields of Extension.
*
* @author johnniang
*/
public interface ExtensionOperator {
@Schema(requiredMode = REQUIRED)
@JsonProperty("apiVersion")
default String getApiVersion() {
final var gvk = getClass().getAnnotation(GVK.class);
if (gvk == null) {
// return null if having no GVK annotation
return null;
}
if (StringUtils.hasText(gvk.group())) {
return gvk.group() + "/" + gvk.version();
}
return gvk.version();
}
@Schema(requiredMode = REQUIRED)
@JsonProperty("kind")
default String getKind() {
final var gvk = getClass().getAnnotation(GVK.class);
if (gvk == null) {
// return null if having no GVK annotation
return null;
}
return gvk.kind();
}
@Schema(requiredMode = REQUIRED, implementation = Metadata.class)
@JsonProperty("metadata")
MetadataOperator getMetadata();
void setApiVersion(String apiVersion);
void setKind(String kind);
void setMetadata(MetadataOperator metadata);
/**
* Sets GroupVersionKind of the Extension.
*
* @param gvk is GroupVersionKind data.
*/
default void groupVersionKind(GroupVersionKind gvk) {
setApiVersion(gvk.groupVersion().toString());
setKind(gvk.kind());
}
/**
* Gets GroupVersionKind of the Extension.
*
* @return GroupVersionKind of the Extension.
*/
@JsonIgnore
default GroupVersionKind groupVersionKind() {
return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind());
}
static <T extends ExtensionOperator> Predicate<T> isNotDeleted() {
return ext -> ext.getMetadata().getDeletionTimestamp() == null;
}
static boolean isDeleted(ExtensionOperator extension) {
return ExtensionUtil.isDeleted(extension);
}
}

View File

@ -0,0 +1,61 @@
package run.halo.app.extension;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.domain.Sort;
import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.index.query.QueryFactory;
public enum ExtensionUtil {
;
public static boolean isDeleted(ExtensionOperator extension) {
return extension.getMetadata() != null
&& extension.getMetadata().getDeletionTimestamp() != null;
}
public static boolean addFinalizers(MetadataOperator metadata, Set<String> finalizers) {
var modifiableFinalizers = new HashSet<>(
metadata.getFinalizers() == null ? Collections.emptySet() : metadata.getFinalizers());
var added = modifiableFinalizers.addAll(finalizers);
if (added) {
metadata.setFinalizers(modifiableFinalizers);
}
return added;
}
public static boolean removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {
if (metadata.getFinalizers() == null) {
return false;
}
var existingFinalizers = new HashSet<>(metadata.getFinalizers());
var removed = existingFinalizers.removeAll(finalizers);
if (removed) {
metadata.setFinalizers(existingFinalizers);
}
return removed;
}
/**
* Query for not deleting.
*
* @return Query
*/
public static Query notDeleting() {
return QueryFactory.isNull("metadata.deletionTimestamp");
}
/**
* Default sort by creation timestamp desc and name asc.
*
* @return Sort
*/
public static Sort defaultSort() {
return Sort.by(
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.asc("metadata.name")
);
}
}

View File

@ -0,0 +1,42 @@
package run.halo.app.extension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* GVK is an annotation to specific metadata of Extension.
*
* @author johnniang
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface GVK {
/**
* @return group name of Extension.
*/
String group();
/**
* @return version name of Extension.
*/
String version();
/**
* @return kind name of Extension.
*/
String kind();
/**
* @return plural name of Extension.
*/
String plural();
/**
* @return singular name of Extension.
*/
String singular();
}

View File

@ -0,0 +1,11 @@
package run.halo.app.extension;
/**
* GroupKind contains group and kind data only.
*
* @param group is group name of Extension.
* @param kind is kind name of Extension.
* @author johnniang
*/
public record GroupKind(String group, String kind) {
}

View File

@ -0,0 +1,40 @@
package run.halo.app.extension;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* GroupVersion contains group and version name of an Extension only.
*
* @param group is group name of Extension.
* @param version is version name of Extension.
* @author johnniang
*/
public record GroupVersion(String group, String version) {
@Override
public String toString() {
return StringUtils.hasText(group) ? group + "/" + version : version;
}
/**
* Parses APIVersion into GroupVersion record.
*
* @param apiVersion must not be blank.
* 1. If the given apiVersion does not contain any "/", we treat the group is empty.
* 2. If the given apiVersion contains more than 1 "/", we will throw an
* IllegalArgumentException.
* @return record contains group and version.
*/
public static GroupVersion parseAPIVersion(String apiVersion) {
Assert.hasText(apiVersion, "API version must not be blank");
var groupVersion = apiVersion.split("/");
return switch (groupVersion.length) {
case 1 -> new GroupVersion("", apiVersion);
case 2 -> new GroupVersion(groupVersion[0], groupVersion[1]);
default ->
throw new IllegalArgumentException("Unexpected APIVersion string: " + apiVersion);
};
}
}

View File

@ -0,0 +1,64 @@
package run.halo.app.extension;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* GroupVersionKind contains group, version and kind name of an Extension.
*
* @param group is group name of Extension.
* @param version is version name of Extension.
* @param kind is kind name of Extension.
* @author johnniang
*/
public record GroupVersionKind(String group, String version, String kind) {
public GroupVersionKind {
Assert.hasText(version, "Version must not be blank");
Assert.hasText(kind, "Kind must not be blank");
}
/**
* Gets group and version name of Extension.
*
* @return group and version name of Extension.
*/
public GroupVersion groupVersion() {
return new GroupVersion(group, version);
}
public GroupKind groupKind() {
return new GroupKind(group, kind);
}
public boolean hasGroup() {
return StringUtils.hasText(group);
}
/**
* Composes GroupVersionKind from API version and kind name.
*
* @param apiVersion is API version. Like "core.halo.run/v1alpha1"
* @param kind is kind name of Extension.
* @return GroupVersionKind of an Extension.
*/
public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String kind) {
Assert.hasText(kind, "Kind must not be blank");
var gv = GroupVersion.parseAPIVersion(apiVersion);
return new GroupVersionKind(gv.group(), gv.version(), kind);
}
public static <T extends Extension> GroupVersionKind fromExtension(Class<T> extension) {
GVK gvk = extension.getAnnotation(GVK.class);
return new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());
}
@Override
public String toString() {
if (hasGroup()) {
return group + "/" + version + "/" + kind;
}
return version + "/" + kind;
}
}

View File

@ -0,0 +1,259 @@
package run.halo.app.extension;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* JsonExtension is representation an extension using ObjectNode. This extension is preparing for
* patching in the future.
*
* @author johnniang
*/
@JsonSerialize(using = JsonExtension.ObjectNodeExtensionSerializer.class)
@JsonDeserialize(using = JsonExtension.ObjectNodeExtensionDeSerializer.class)
public class JsonExtension implements Extension {
private final ObjectMapper objectMapper;
private final ObjectNode objectNode;
public JsonExtension(ObjectMapper objectMapper) {
this(objectMapper, objectMapper.createObjectNode());
}
public JsonExtension(ObjectMapper objectMapper, ObjectNode objectNode) {
this.objectMapper = objectMapper;
this.objectNode = objectNode;
}
public JsonExtension(ObjectMapper objectMapper, Extension e) {
this(objectMapper, (ObjectNode) objectMapper.valueToTree(e));
}
@Override
public MetadataOperator getMetadata() {
var metadataNode = objectNode.get("metadata");
if (metadataNode == null) {
return null;
}
return new ObjectNodeMetadata((ObjectNode) metadataNode);
}
@Override
public String getApiVersion() {
var apiVersionNode = objectNode.get("apiVersion");
return apiVersionNode == null ? null : apiVersionNode.asText();
}
@Override
public String getKind() {
return objectNode.get("kind").asText();
}
@Override
public void setApiVersion(String apiVersion) {
objectNode.set("apiVersion", new TextNode(apiVersion));
}
@Override
public void setKind(String kind) {
objectNode.set("kind", new TextNode(kind));
}
@Override
public void setMetadata(MetadataOperator metadata) {
objectNode.set("metadata", objectMapper.valueToTree(metadata));
}
public static class ObjectNodeExtensionSerializer extends JsonSerializer<JsonExtension> {
@Override
public void serialize(JsonExtension value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeTree(value.objectNode);
}
}
public static class ObjectNodeExtensionDeSerializer
extends JsonDeserializer<JsonExtension> {
@Override
public JsonExtension deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
var mapper = (ObjectMapper) p.getCodec();
var treeNode = mapper.readTree(p);
return new JsonExtension(mapper, (ObjectNode) treeNode);
}
}
/**
* Get internal representation.
*
* @return internal representation
*/
public ObjectNode getInternal() {
return objectNode;
}
/**
* Get object mapper.
*
* @return object mapper
*/
public ObjectMapper getObjectMapper() {
return objectMapper;
}
public MetadataOperator getMetadataOrCreate() {
var metadataNode = objectMapper.createObjectNode();
objectNode.set("metadata", metadataNode);
return new ObjectNodeMetadata(metadataNode);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JsonExtension that = (JsonExtension) o;
return Objects.equals(objectNode, that.objectNode);
}
@Override
public int hashCode() {
return Objects.hash(objectNode);
}
class ObjectNodeMetadata implements MetadataOperator {
private final ObjectNode objectNode;
public ObjectNodeMetadata(ObjectNode objectNode) {
this.objectNode = objectNode;
}
@Override
public String getName() {
var nameNode = objectNode.get("name");
return objectMapper.convertValue(nameNode, String.class);
}
@Override
public String getGenerateName() {
var generateNameNode = objectNode.get("generateName");
return objectMapper.convertValue(generateNameNode, String.class);
}
@Override
public Map<String, String> getLabels() {
var labelsNode = objectNode.get("labels");
return objectMapper.convertValue(labelsNode, new TypeReference<>() {
});
}
@Override
public Map<String, String> getAnnotations() {
var annotationsNode = objectNode.get("annotations");
return objectMapper.convertValue(annotationsNode, new TypeReference<>() {
});
}
@Override
public Long getVersion() {
JsonNode versionNode = objectNode.get("version");
return objectMapper.convertValue(versionNode, Long.class);
}
@Override
public Instant getCreationTimestamp() {
return objectMapper.convertValue(objectNode.get("creationTimestamp"), Instant.class);
}
@Override
public Instant getDeletionTimestamp() {
return objectMapper.convertValue(objectNode.get("deletionTimestamp"), Instant.class);
}
@Override
public Set<String> getFinalizers() {
return objectMapper.convertValue(objectNode.get("finalizers"), new TypeReference<>() {
});
}
@Override
public void setName(String name) {
if (name != null) {
objectNode.set("name", TextNode.valueOf(name));
}
}
@Override
public void setGenerateName(String generateName) {
if (generateName != null) {
objectNode.set("generateName", TextNode.valueOf(generateName));
}
}
@Override
public void setLabels(Map<String, String> labels) {
if (labels != null) {
objectNode.set("labels", objectMapper.valueToTree(labels));
}
}
@Override
public void setAnnotations(Map<String, String> annotations) {
if (annotations != null) {
objectNode.set("annotations", objectMapper.valueToTree(annotations));
}
}
@Override
public void setVersion(Long version) {
if (version != null) {
objectNode.set("version", LongNode.valueOf(version));
}
}
@Override
public void setCreationTimestamp(Instant creationTimestamp) {
if (creationTimestamp != null) {
objectNode.set("creationTimestamp", objectMapper.valueToTree(creationTimestamp));
}
}
@Override
public void setDeletionTimestamp(Instant deletionTimestamp) {
if (deletionTimestamp != null) {
objectNode.set("deletionTimestamp", objectMapper.valueToTree(deletionTimestamp));
}
}
@Override
public void setFinalizers(Set<String> finalizers) {
if (finalizers != null) {
objectNode.set("finalizers", objectMapper.valueToTree(finalizers));
}
}
}
}

View File

@ -0,0 +1,127 @@
package run.halo.app.extension;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;
import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.extension.router.selector.SelectorMatcher;
@Data
@Accessors(chain = true)
public class ListOptions {
private LabelSelector labelSelector;
private FieldSelector fieldSelector;
@Override
public String toString() {
var sb = new StringBuilder();
if (fieldSelector != null) {
var query = fieldSelector.query().toString();
sb.append("fieldSelector: ")
.append(query.startsWith("(") ? query : "(" + query + ")");
}
if (labelSelector != null) {
if (!sb.isEmpty()) {
sb.append(", ");
}
sb.append("labelSelector: (").append(labelSelector).append(")");
}
return sb.toString();
}
public static ListOptionsBuilder builder() {
return new ListOptionsBuilder();
}
public static ListOptionsBuilder builder(ListOptions listOptions) {
return new ListOptionsBuilder(listOptions);
}
public static class ListOptionsBuilder {
private LabelSelectorBuilder labelSelectorBuilder;
private Query query;
public ListOptionsBuilder() {
}
/**
* Create a new list options builder with the given list options.
*/
public ListOptionsBuilder(ListOptions listOptions) {
if (listOptions.getLabelSelector() != null) {
this.labelSelectorBuilder = new LabelSelectorBuilder(
listOptions.getLabelSelector().getMatchers(), this);
}
if (listOptions.getFieldSelector() != null) {
this.query = listOptions.getFieldSelector().query();
}
}
/**
* Create a new label selector builder.
*/
public LabelSelectorBuilder labelSelector() {
if (labelSelectorBuilder == null) {
labelSelectorBuilder = new LabelSelectorBuilder(this);
}
return labelSelectorBuilder;
}
public ListOptionsBuilder fieldQuery(Query query) {
this.query = query;
return this;
}
/**
* And the given query to the current query.
*/
public ListOptionsBuilder andQuery(Query query) {
this.query = (this.query == null ? query : QueryFactory.and(this.query, query));
return this;
}
/**
* Or the given query to the current query.
*/
public ListOptionsBuilder orQuery(Query query) {
this.query = (this.query == null ? query : QueryFactory.or(this.query, query));
return this;
}
/**
* Build the list options.
*/
public ListOptions build() {
var listOptions = new ListOptions();
if (labelSelectorBuilder != null) {
listOptions.setLabelSelector(labelSelectorBuilder.build());
}
if (query != null) {
listOptions.setFieldSelector(FieldSelector.of(query));
}
return listOptions;
}
}
public static class LabelSelectorBuilder
extends LabelSelector.LabelSelectorBuilder<LabelSelectorBuilder> {
private final ListOptionsBuilder listOptionsBuilder;
public LabelSelectorBuilder(List<SelectorMatcher> givenMatchers,
ListOptionsBuilder listOptionsBuilder) {
super(givenMatchers);
this.listOptionsBuilder = listOptionsBuilder;
}
public LabelSelectorBuilder(ListOptionsBuilder listOptionsBuilder) {
this.listOptionsBuilder = listOptionsBuilder;
}
public ListOptionsBuilder end() {
return this.listOptionsBuilder;
}
}
}

View File

@ -0,0 +1,161 @@
package run.halo.app.extension;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import lombok.Data;
import org.springframework.util.Assert;
import run.halo.app.infra.utils.GenericClassUtils;
@Data
public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
@Schema(description = "Page number, starts from 1. If not set or equal to 0, it means no "
+ "pagination.", requiredMode = REQUIRED)
private final int page;
@Schema(description = "Size of each page. If not set or equal to 0, it means no pagination.",
requiredMode = REQUIRED)
private final int size;
@Schema(description = "Total elements.", requiredMode = REQUIRED)
private final long total;
@Schema(description = "A chunk of items.", requiredMode = REQUIRED)
private final List<T> items;
public ListResult(int page, int size, long total, List<T> items) {
Assert.isTrue(total >= 0, "Total elements must be greater than or equal to 0");
if (page < 0) {
page = 0;
}
if (size < 0) {
size = 0;
}
if (items == null) {
items = Collections.emptyList();
}
this.page = page;
this.size = size;
this.total = total;
this.items = items;
}
public ListResult(List<T> items) {
this(0, 0, items.size(), items);
}
@Schema(description = "Indicates whether current page is the first page.",
requiredMode = REQUIRED)
public boolean isFirst() {
return !hasPrevious();
}
@Schema(description = "Indicates whether current page is the last page.",
requiredMode = REQUIRED)
public boolean isLast() {
return !hasNext();
}
@Schema(description = "Indicates whether current page has previous page.",
requiredMode = REQUIRED)
@JsonProperty("hasNext")
public boolean hasNext() {
if (page <= 0) {
return false;
}
return page < getTotalPages();
}
@Schema(description = "Indicates whether current page has previous page.",
requiredMode = REQUIRED)
@JsonProperty("hasPrevious")
public boolean hasPrevious() {
return page > 1;
}
@Override
public Iterator<T> iterator() {
return items.iterator();
}
@Schema(description = "Indicates total pages.", requiredMode = REQUIRED)
@JsonProperty("totalPages")
public long getTotalPages() {
return size == 0 ? 1 : (total + size - 1) / size;
}
/**
* Generate generic ListResult class. Like {@code ListResult<User>}, {@code ListResult<Post>},
* etc.
*
* @param scheme scheme of the generic type.
* @return generic ListResult class.
*/
public static Class<?> generateGenericClass(Scheme scheme) {
return GenericClassUtils.generateConcreteClass(ListResult.class,
scheme.type(),
() -> {
var pkgName = scheme.type().getPackageName();
return pkgName + '.' + scheme.groupVersionKind().kind() + "List";
});
}
/**
* Generate generic ListResult class. Like {@code ListResult<User>}, {@code ListResult<Post>},
* etc.
*
* @param type the generic type of {@link ListResult}.
* @return generic ListResult class.
*/
public static <T> Class<?> generateGenericClass(Class<T> type) {
return GenericClassUtils.generateConcreteClass(ListResult.class, type,
() -> type.getName() + "List");
}
public static <T> ListResult<T> emptyResult() {
return new ListResult<>(List.of());
}
/**
* Manually paginate the List collection.
*/
public static <T> List<T> subList(List<T> list, int page, int size) {
if (page < 1) {
page = 1;
}
if (size < 1) {
return list;
}
List<T> listSort = new ArrayList<>();
int total = list.size();
int pageStart = page == 1 ? 0 : (page - 1) * size;
int pageEnd = Math.min(total, page * size);
if (total > pageStart) {
listSort = list.subList(pageStart, pageEnd);
}
return listSort;
}
/**
* Gets the first element of the list result.
*/
public static <T> Optional<T> first(ListResult<T> listResult) {
return Optional.ofNullable(listResult)
.map(ListResult::getItems)
.map(list -> list.isEmpty() ? null : list.get(0));
}
@Override
public Stream<T> get() {
return items.stream();
}
}

View File

@ -0,0 +1,55 @@
package run.halo.app.extension;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Metadata of Extension.
*
* @author johnniang
*/
@Data
@EqualsAndHashCode(exclude = "version")
public class Metadata implements MetadataOperator {
/**
* Metadata name. The name is unique globally.
*/
private String name;
/**
* Generate name is for generating metadata name automatically.
*/
private String generateName;
/**
* Labels are like key-value format.
*/
private Map<String, String> labels;
/**
* Annotations are like key-value format.
*/
private Map<String, String> annotations;
/**
* Current version of the Extension. It will be bumped up every update.
*/
private Long version;
/**
* Creation timestamp of the Extension.
*/
private Instant creationTimestamp;
/**
* Deletion timestamp of the Extension.
*/
private Instant deletionTimestamp;
private Set<String> finalizers;
}

Some files were not shown because too many files have changed in this diff Show More